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_with_prefill,
9        render_chat_prompt_with_tools_and_model_template_compatibility_with_prefill,
10        ChatTemplateOptions, 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,
34    ExecutionResourceAuthority, FerrumConfigBuilder, FerrumError as Error, FerrumProfileEvent,
35    FinishReason, InferenceExecutionEvidence, InferenceRequest, InferenceResponse, ModelId,
36    ModelOutputProtocol, NativeChatOutputProjector, ParsedReasoningResponse, Priority,
37    ProcessMemoryObservation, ProcessMemorySample, ProcessMemorySampler, ProfileEntrypoint,
38    ProfileError, ProfileEventKind, ProfileStatus, ReplayReference, RequestId,
39    ResolvedFerrumConfig, ResourceAction, ResourceTraceEvent, ResponseCompletionBoundary,
40    RuntimeConfigSnapshot, SamplingParams, StructuredOutputStart, TokenId, TokenUsage,
41    DEFAULT_CHAT_REPETITION_PENALTY, DEFAULT_MAX_TOKENS_METADATA_KEY,
42    OBSERVABILITY_PROFILE_SCHEMA_VERSION, PROMPT_OPENED_REASONING_METADATA_KEY, THINK_END_TAG,
43    THINK_START_TAG,
44};
45use sha2::{Digest, Sha256};
46use std::{
47    collections::{BTreeMap, HashMap},
48    error::Error as StdError,
49    fs,
50    path::{Path, PathBuf},
51    sync::{
52        atomic::{AtomicBool, Ordering},
53        Arc, Mutex, OnceLock,
54    },
55    time::Instant,
56};
57use tokio::sync::{mpsc, Notify};
58use tokio_stream::StreamExt;
59use tower::ServiceBuilder;
60use tower_http::{cors::CorsLayer, trace::TraceLayer};
61use tracing::{debug, error, info, span, warn, Level};
62use uuid::Uuid;
63
64mod responses;
65
66const DEFAULT_SAMPLING_TEMPERATURE: f32 = 0.0;
67const DEFAULT_SAMPLING_TOP_P: f32 = 1.0;
68const DEFAULT_COMPLETION_MAX_TOKENS: u32 = 4096;
69const INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY: &str = "ferrum_initial_forbidden_token_texts";
70const DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH: u64 = 128;
71const MAX_CACHED_JSON_SCHEMA_VALIDATORS: usize = 64;
72const INITIAL_STRUCTURED_CALL_FORBIDDEN_TOKEN_TEXTS: &[&str] =
73    &["<|im_end|>", "<|endoftext|>", "<|eot_id|>", "</s>"];
74const FERRUM_SESSION_HEADER: &str = "x-ferrum-session";
75static JSON_SCHEMA_VALIDATOR_CACHE: OnceLock<Mutex<HashMap<String, Arc<jsonschema::Validator>>>> =
76    OnceLock::new();
77
78/// Product defaults used when an OpenAI chat request omits sampling fields.
79/// The engine composition root consumes the same typed value so its resolved
80/// startup plan cannot describe different defaults from the HTTP endpoint.
81pub fn default_chat_sampling_params() -> SamplingParams {
82    SamplingParams {
83        max_tokens: DEFAULT_COMPLETION_MAX_TOKENS as usize,
84        temperature: DEFAULT_SAMPLING_TEMPERATURE,
85        top_p: DEFAULT_SAMPLING_TOP_P,
86        repetition_penalty: DEFAULT_CHAT_REPETITION_PENALTY,
87        ..SamplingParams::default()
88    }
89}
90
91#[derive(Debug, Clone)]
92struct CachePolicy {
93    prefix_cache_enabled: bool,
94    session_cache_mode: String,
95    session_cache_max_entries: usize,
96    session_cache_max_tokens: usize,
97}
98
99impl CachePolicy {
100    fn current() -> Self {
101        Self {
102            prefix_cache_enabled: env_bool("FERRUM_PREFIX_CACHE_PRODUCT")
103                .or_else(|| env_bool("FERRUM_PREFIX_CACHE_REQUESTED"))
104                .or_else(|| env_bool("FERRUM_PREFIX_CACHE"))
105                .unwrap_or(false),
106            session_cache_mode: std::env::var("FERRUM_SESSION_CACHE")
107                .unwrap_or_else(|_| "off".to_string())
108                .to_ascii_lowercase(),
109            session_cache_max_entries: env_usize("FERRUM_SESSION_CACHE_MAX_ENTRIES").unwrap_or(128),
110            session_cache_max_tokens: env_usize("FERRUM_SESSION_CACHE_MAX_TOKENS").unwrap_or(4096),
111        }
112    }
113
114    fn session_memory_enabled(&self) -> bool {
115        self.session_cache_mode == "memory"
116    }
117}
118
119fn env_bool(key: &str) -> Option<bool> {
120    match std::env::var(key).ok()?.to_ascii_lowercase().as_str() {
121        "1" | "true" | "yes" | "on" => Some(true),
122        "0" | "false" | "no" | "off" => Some(false),
123        _ => None,
124    }
125}
126
127fn env_usize(key: &str) -> Option<usize> {
128    std::env::var(key).ok()?.parse().ok()
129}
130
131/// Shared Prometheus recorder handle for rendering metrics.
132static PROM_HANDLE: std::sync::OnceLock<metrics_exporter_prometheus::PrometheusHandle> =
133    std::sync::OnceLock::new();
134
135/// Initialize the Prometheus metrics recorder.
136///
137/// Must be called once before any `metrics::counter!()` / `histogram!()` calls.
138/// Safe to call multiple times — subsequent calls are no-ops.
139pub fn init_prometheus_recorder() {
140    PROM_HANDLE.get_or_init(|| {
141        let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
142        let handle = builder
143            .install_recorder()
144            .expect("Failed to install Prometheus recorder");
145        info!("Prometheus metrics recorder installed");
146        handle
147    });
148}
149
150/// Axum-based server implementation.
151///
152/// The server is built around [`AppState`], which holds an optional
153/// engine per modality. Handlers fault to 503 when the modality they
154/// need isn't loaded, instead of running stub error logic.
155pub struct AxumServer {
156    state: AppState,
157    config: ServerConfig,
158    lifecycle: Arc<AxumServerLifecycle>,
159}
160
161#[derive(Default)]
162struct AxumServerLifecycle {
163    shutdown_requested: AtomicBool,
164    running: AtomicBool,
165    engines_stopped: AtomicBool,
166    shutdown_notify: Notify,
167    stopped_notify: Notify,
168    stop_lock: tokio::sync::Mutex<()>,
169}
170
171impl AxumServerLifecycle {
172    fn request_shutdown(&self) {
173        self.shutdown_requested.store(true, Ordering::Release);
174        self.shutdown_notify.notify_waiters();
175    }
176
177    async fn wait_for_shutdown(&self) {
178        while !self.shutdown_requested.load(Ordering::Acquire) {
179            self.shutdown_notify.notified().await;
180        }
181    }
182
183    async fn wait_until_stopped(&self) {
184        while self.running.load(Ordering::Acquire) {
185            self.stopped_notify.notified().await;
186        }
187    }
188}
189
190struct AxumServerRunGuard {
191    lifecycle: Arc<AxumServerLifecycle>,
192}
193
194impl Drop for AxumServerRunGuard {
195    fn drop(&mut self) {
196        self.lifecycle.running.store(false, Ordering::Release);
197        self.lifecycle.stopped_notify.notify_waiters();
198    }
199}
200
201fn single_model_registry(engine_model_id: ModelId, kind: ServedModelKind) -> ServedModelRegistry {
202    let public_name = engine_model_id.to_string();
203    ServedModelRegistry::try_new(engine_model_id, kind, vec![public_name], vec![])
204        .expect("engine config must contain a valid model id")
205}
206
207impl AxumServer {
208    /// Create a server with a fully populated AppState.
209    pub fn from_state(state: AppState) -> Self {
210        Self {
211            state,
212            config: ServerConfig::default(),
213            lifecycle: Arc::new(AxumServerLifecycle::default()),
214        }
215    }
216
217    /// Convenience constructor for an LLM-only server (chat / completions).
218    pub fn from_llm(engine: Arc<dyn LlmInferenceEngine + Send + Sync>) -> Self {
219        Self::from_state(AppState::default().with_llm(engine))
220    }
221
222    /// Convenience constructor for an embedding-only server (`/v1/embeddings`).
223    pub fn from_embed(engine: Arc<dyn EmbedEngine + Send + Sync>) -> Self {
224        Self::from_state(AppState::default().with_embed(engine))
225    }
226
227    /// Convenience constructor for a transcription-only server
228    /// (`/v1/audio/transcriptions`).
229    pub fn from_transcribe(engine: Arc<dyn TranscribeEngine + Send + Sync>) -> Self {
230        Self::from_state(AppState::default().with_transcribe(engine))
231    }
232
233    /// Convenience constructor for a TTS-only server (`/v1/audio/speech`).
234    pub fn from_tts(engine: Arc<dyn TtsEngine + Send + Sync>) -> Self {
235        Self::from_state(AppState::default().with_tts(engine))
236    }
237
238    /// Attach the startup auto-configuration decision trace exposed by
239    /// `/health`. Constructors keep this optional so tests and non-LLM
240    /// deployments can use the server without a model-specific resolver.
241    pub fn with_auto_config(mut self, auto_config: ResolvedFerrumConfig) -> Self {
242        self.state = self.state.with_auto_config(auto_config);
243        self
244    }
245
246    /// Attach the loaded model's prompt template, if available. This keeps
247    /// OpenAI request aliases from driving prompt-family selection.
248    pub fn with_prompt_template(mut self, prompt_template: Option<ModelChatTemplate>) -> Self {
249        self.state = self.state.with_prompt_template(prompt_template);
250        self
251    }
252
253    /// Set the server-wide reasoning default. Explicit standard effort or
254    /// `chat_template_kwargs.enable_thinking` overrides this default.
255    pub fn with_default_enable_thinking(mut self, enable_thinking: Option<bool>) -> Self {
256        self.state = self.state.with_default_enable_thinking(enable_thinking);
257        self
258    }
259
260    /// Control the compatibility retry for model templates that reject
261    /// non-leading system messages. It is enabled by default.
262    pub fn with_interleaved_system_coalescing(mut self, enabled: bool) -> Self {
263        self.state = self.state.with_interleaved_system_coalescing(enabled);
264        self
265    }
266
267    /// Install the public OpenAI model namespace used for request routing and
268    /// `/v1/models`. The registry keeps public aliases separate from the
269    /// engine's internal model id.
270    pub fn with_served_model_registry(mut self, registry: ServedModelRegistry) -> Self {
271        self.state = self.state.with_served_model_registry(registry);
272        self
273    }
274
275    /// Attach startup-loaded LoRA adapter model ids.
276    pub fn with_lora_adapters(
277        mut self,
278        base_model_id: impl Into<String>,
279        adapters: Vec<LoraAdapterModel>,
280    ) -> ferrum_types::Result<Self> {
281        let base_model_id = base_model_id.into();
282        let registry = if self.state.served_model_registry.is_empty() {
283            ServedModelRegistry::try_new(
284                base_model_id.clone(),
285                ServedModelKind::Llm,
286                vec![base_model_id],
287                adapters,
288            )
289        } else {
290            self.state
291                .served_model_registry
292                .try_with_lora_adapters(&base_model_id, adapters)
293        }
294        .map_err(|error| Error::config(error.to_string()))?;
295        self.state = self.state.with_served_model_registry(registry);
296        Ok(self)
297    }
298
299    async fn shutdown_loaded_engines(&self) -> ferrum_types::Result<()> {
300        let mut first_error = None;
301        if let Some(engine) = &self.state.llm {
302            if let Err(error) = engine.shutdown().await {
303                first_error = Some(error);
304            }
305        }
306        if let Some(engine) = &self.state.embed {
307            if let Err(error) = engine.shutdown().await {
308                if first_error.is_none() {
309                    first_error = Some(error);
310                }
311            }
312        }
313        if let Some(engine) = &self.state.transcribe {
314            if let Err(error) = engine.shutdown().await {
315                if first_error.is_none() {
316                    first_error = Some(error);
317                }
318            }
319        }
320        if let Some(engine) = &self.state.tts {
321            if let Err(error) = engine.shutdown().await {
322                if first_error.is_none() {
323                    first_error = Some(error);
324                }
325            }
326        }
327        first_error.map_or(Ok(()), Err)
328    }
329
330    /// Build the router with all routes
331    #[allow(dead_code)]
332    fn build_router(&self) -> Router {
333        self.build_router_with_state(self.state.clone())
334    }
335
336    fn build_router_with_state(&self, app_state: AppState) -> Router {
337        Router::new()
338            // OpenAI API routes
339            .route("/v1/chat/completions", post(chat_completions_handler))
340            .route("/v1/responses", post(responses::responses_handler))
341            .route("/v1/completions", post(completions_handler))
342            .route("/v1/embeddings", post(embeddings_handler))
343            .route("/v1/audio/transcriptions", post(transcriptions_handler))
344            .route("/v1/audio/speech", post(speech_handler))
345            .route("/v1/models", get(models_handler))
346            // Health & observability
347            .route("/health", get(health_handler))
348            .route("/metrics", get(metrics_handler))
349            .route("/", get(root_handler))
350            // Apply middleware
351            .layer(
352                ServiceBuilder::new()
353                    .layer(TraceLayer::new_for_http())
354                    .layer(CorsLayer::permissive()), // For MVP, allow all origins
355            )
356            .with_state(app_state)
357    }
358}
359
360/// Application state shared across handlers — one optional engine per
361/// modality. Handlers reach into the field they need and 503 when it's
362/// not loaded.
363#[derive(Clone, Default)]
364pub struct AppState {
365    pub llm: Option<Arc<dyn LlmInferenceEngine + Send + Sync>>,
366    pub embed: Option<Arc<dyn EmbedEngine + Send + Sync>>,
367    pub transcribe: Option<Arc<dyn TranscribeEngine + Send + Sync>>,
368    pub tts: Option<Arc<dyn TtsEngine + Send + Sync>>,
369    pub auto_config: Option<ResolvedFerrumConfig>,
370    pub prompt_template: Option<Arc<ModelChatTemplate>>,
371    pub default_enable_thinking: Option<bool>,
372    interleaved_system_coalescing: Option<bool>,
373    pub served_model_registry: Arc<ServedModelRegistry>,
374    pub request_dump_dir: Option<Arc<PathBuf>>,
375    pub profile_jsonl: Option<Arc<PathBuf>>,
376    pub profile_detail: ferrum_types::ObservabilityProfileDetail,
377    pub memory_profile_jsonl: Option<Arc<PathBuf>>,
378    pub first_request_memory_recorded: Arc<AtomicBool>,
379    cache: Arc<CacheRuntimeState>,
380}
381
382impl AppState {
383    fn record_prefix_prompt(&self, prompt: &str, policy: &CachePolicy) {
384        if self.llm.as_ref().is_some_and(|engine| {
385            engine.execution_resource_authority() == ExecutionResourceAuthority::PlanRuntime
386        }) {
387            // Native cache health already reports actual reuse or an unsupported
388            // plan. The legacy text-LCP estimate would only duplicate full raw
389            // prompts and scan them under a lock, outside the runtime budget.
390            return;
391        }
392        self.cache.record_prefix_prompt(prompt, policy);
393    }
394
395    pub fn with_llm(mut self, engine: Arc<dyn LlmInferenceEngine + Send + Sync>) -> Self {
396        if self.served_model_registry.is_empty() {
397            self.served_model_registry = Arc::new(single_model_registry(
398                engine.config().model.model_id.clone(),
399                ServedModelKind::Llm,
400            ));
401        }
402        self.llm = Some(engine);
403        self
404    }
405    pub fn with_embed(mut self, engine: Arc<dyn EmbedEngine + Send + Sync>) -> Self {
406        if self.served_model_registry.is_empty() {
407            self.served_model_registry = Arc::new(single_model_registry(
408                engine.config().model.model_id.clone(),
409                ServedModelKind::Embedding,
410            ));
411        }
412        self.embed = Some(engine);
413        self
414    }
415    pub fn with_transcribe(mut self, engine: Arc<dyn TranscribeEngine + Send + Sync>) -> Self {
416        if self.served_model_registry.is_empty() {
417            self.served_model_registry = Arc::new(single_model_registry(
418                engine.config().model.model_id.clone(),
419                ServedModelKind::Transcription,
420            ));
421        }
422        self.transcribe = Some(engine);
423        self
424    }
425    pub fn with_tts(mut self, engine: Arc<dyn TtsEngine + Send + Sync>) -> Self {
426        if self.served_model_registry.is_empty() {
427            self.served_model_registry = Arc::new(single_model_registry(
428                engine.config().model.model_id.clone(),
429                ServedModelKind::Speech,
430            ));
431        }
432        self.tts = Some(engine);
433        self
434    }
435
436    pub fn with_auto_config(mut self, auto_config: ResolvedFerrumConfig) -> Self {
437        self.auto_config = Some(auto_config);
438        self
439    }
440
441    pub fn with_prompt_template(mut self, prompt_template: Option<ModelChatTemplate>) -> Self {
442        self.prompt_template = prompt_template.map(Arc::new);
443        self
444    }
445
446    pub fn with_default_enable_thinking(mut self, enable_thinking: Option<bool>) -> Self {
447        self.default_enable_thinking = enable_thinking;
448        self
449    }
450
451    pub fn with_interleaved_system_coalescing(mut self, enabled: bool) -> Self {
452        self.interleaved_system_coalescing = Some(enabled);
453        self
454    }
455
456    pub fn with_served_model_registry(mut self, registry: ServedModelRegistry) -> Self {
457        self.served_model_registry = Arc::new(registry);
458        self
459    }
460
461    pub fn with_request_dump_dir(mut self, request_dump_dir: Option<PathBuf>) -> Self {
462        self.request_dump_dir = request_dump_dir.map(Arc::new);
463        self
464    }
465
466    pub fn with_profile_jsonl(mut self, profile_jsonl: Option<PathBuf>) -> Self {
467        self.profile_jsonl = profile_jsonl.map(Arc::new);
468        self
469    }
470
471    pub fn with_profile_detail(
472        mut self,
473        profile_detail: ferrum_types::ObservabilityProfileDetail,
474    ) -> Self {
475        self.profile_detail = profile_detail;
476        self
477    }
478
479    pub fn with_memory_profile_jsonl(mut self, memory_profile_jsonl: Option<PathBuf>) -> Self {
480        self.memory_profile_jsonl = memory_profile_jsonl.map(Arc::new);
481        self
482    }
483
484    /// Async aggregated status across whichever modality is loaded.
485    /// In single-modality deployments (current CLI), exactly one is Some.
486    async fn status(&self) -> EngineStatus {
487        if let Some(e) = &self.llm {
488            return e.status().await;
489        }
490        if let Some(e) = &self.embed {
491            return e.status().await;
492        }
493        if let Some(e) = &self.transcribe {
494            return e.status().await;
495        }
496        if let Some(e) = &self.tts {
497            return e.status().await;
498        }
499        EngineStatus {
500            is_ready: false,
501            loaded_models: vec![],
502            active_requests: 0,
503            queued_requests: 0,
504            memory_usage: ferrum_types::MemoryUsage {
505                total_bytes: 0,
506                used_bytes: 0,
507                free_bytes: 0,
508                gpu_memory_bytes: None,
509                cpu_memory_bytes: None,
510                cache_memory_bytes: 0,
511                utilization_percent: 0.0,
512            },
513            uptime_seconds: 0,
514            last_heartbeat: chrono::Utc::now(),
515            version: env!("CARGO_PKG_VERSION").to_string(),
516        }
517    }
518
519    fn metrics(&self) -> EngineMetrics {
520        if let Some(e) = &self.llm {
521            return e.metrics();
522        }
523        if let Some(e) = &self.embed {
524            return e.metrics();
525        }
526        if let Some(e) = &self.transcribe {
527            return e.metrics();
528        }
529        if let Some(e) = &self.tts {
530            return e.metrics();
531        }
532        EngineMetrics {
533            total_requests: 0,
534            successful_requests: 0,
535            failed_requests: 0,
536            avg_request_latency_ms: 0.0,
537            p95_request_latency_ms: 0.0,
538            p99_request_latency_ms: 0.0,
539            throughput_rps: 0.0,
540            tokens_per_second: 0.0,
541            queue_metrics: Default::default(),
542            resource_utilization: Default::default(),
543            error_stats: Default::default(),
544            performance_breakdown: Default::default(),
545        }
546    }
547}
548
549#[derive(Default)]
550struct CacheRuntimeState {
551    stats: Mutex<CacheStats>,
552    prefix_prompts: Mutex<HashMap<String, usize>>,
553    sessions: Mutex<HashMap<String, Vec<ChatMessage>>>,
554}
555
556#[derive(Debug, Clone, Default)]
557struct CacheStats {
558    prefix_hits: u64,
559    prefix_misses: u64,
560    prefix_evictions: u64,
561    prefix_saved_prefill_tokens: u64,
562    prefix_entries: u64,
563    prefix_bytes: u64,
564    session_hits: u64,
565    session_misses: u64,
566    session_evictions: u64,
567    session_entries: u64,
568    session_tokens: u64,
569}
570
571#[derive(Clone)]
572struct SessionContext {
573    id: String,
574    prior_messages: Vec<ChatMessage>,
575    incoming_messages: Vec<ChatMessage>,
576}
577
578impl CacheRuntimeState {
579    fn record_prefix_prompt(&self, prompt: &str, policy: &CachePolicy) {
580        if !policy.prefix_cache_enabled {
581            return;
582        }
583
584        let prompt_tokens = approx_tokens(prompt);
585        let mut prompts = self.prefix_prompts.lock().expect("prefix cache lock");
586        let saved_tokens = prompts
587            .keys()
588            .map(|seen| approx_tokens_for_chars(longest_common_prefix_chars(seen, prompt)))
589            .max()
590            .unwrap_or(0);
591
592        let mut stats = self.stats.lock().expect("cache stats lock");
593        if saved_tokens > 0 {
594            stats.prefix_hits += 1;
595            stats.prefix_saved_prefill_tokens += saved_tokens as u64;
596        } else {
597            stats.prefix_misses += 1;
598        }
599
600        let max_entries = policy.session_cache_max_entries.max(1);
601        if !prompts.contains_key(prompt) && prompts.len() >= max_entries {
602            if let Some(key) = prompts.keys().next().cloned() {
603                prompts.remove(&key);
604                stats.prefix_evictions += 1;
605            }
606        }
607        prompts.insert(prompt.to_string(), prompt_tokens);
608        stats.prefix_entries = prompts.len() as u64;
609        stats.prefix_bytes = prompts.keys().map(|key| key.len() as u64).sum();
610    }
611
612    fn prepare_session_request(
613        &self,
614        request: &mut ChatCompletionsRequest,
615        headers: &HeaderMap,
616        policy: &CachePolicy,
617    ) -> Option<SessionContext> {
618        let session_id = request_session_id(headers, request)?;
619        if !policy.session_memory_enabled() {
620            return None;
621        }
622
623        let incoming_messages = request.messages.clone();
624        let prior_messages = {
625            let sessions = self.sessions.lock().expect("session cache lock");
626            sessions.get(&session_id).cloned().unwrap_or_default()
627        };
628        {
629            let mut stats = self.stats.lock().expect("cache stats lock");
630            if prior_messages.is_empty() {
631                stats.session_misses += 1;
632            } else {
633                stats.session_hits += 1;
634                let mut merged = prior_messages.clone();
635                merged.extend(request.messages.clone());
636                request.messages = merged;
637            }
638        }
639
640        Some(SessionContext {
641            id: session_id,
642            prior_messages,
643            incoming_messages,
644        })
645    }
646
647    fn update_session(
648        &self,
649        context: Option<SessionContext>,
650        assistant_message: ChatMessage,
651        policy: &CachePolicy,
652    ) {
653        let Some(context) = context else {
654            return;
655        };
656        if !policy.session_memory_enabled() {
657            return;
658        }
659
660        let mut history = context.prior_messages;
661        history.extend(context.incoming_messages);
662        history.push(assistant_message);
663        trim_messages_to_token_budget(&mut history, policy.session_cache_max_tokens);
664
665        let mut sessions = self.sessions.lock().expect("session cache lock");
666        if !sessions.contains_key(&context.id)
667            && sessions.len() >= policy.session_cache_max_entries.max(1)
668        {
669            if let Some(evict_key) = sessions.keys().next().cloned() {
670                sessions.remove(&evict_key);
671                self.stats
672                    .lock()
673                    .expect("cache stats lock")
674                    .session_evictions += 1;
675            }
676        }
677        sessions.insert(context.id, history);
678
679        let entries = sessions.len() as u64;
680        let tokens = sessions
681            .values()
682            .map(|messages| {
683                messages
684                    .iter()
685                    .map(|msg| approx_tokens(&msg.content))
686                    .sum::<usize>()
687            })
688            .sum::<usize>() as u64;
689        let mut stats = self.stats.lock().expect("cache stats lock");
690        stats.session_entries = entries;
691        stats.session_tokens = tokens;
692    }
693
694    fn stats(&self) -> CacheStats {
695        let mut stats = self.stats.lock().expect("cache stats lock").clone();
696        stats.prefix_entries = self.prefix_prompts.lock().expect("prefix cache lock").len() as u64;
697        let sessions = self.sessions.lock().expect("session cache lock");
698        stats.session_entries = sessions.len() as u64;
699        stats.session_tokens = sessions
700            .values()
701            .map(|messages| {
702                messages
703                    .iter()
704                    .map(|msg| approx_tokens(&msg.content))
705                    .sum::<usize>()
706            })
707            .sum::<usize>() as u64;
708        stats
709    }
710
711    fn health_json(
712        &self,
713        policy: &CachePolicy,
714        engine_prefix_cache: Option<&serde_json::Value>,
715    ) -> serde_json::Value {
716        let stats = self.stats();
717        let prefix_hits = engine_u64(engine_prefix_cache, "hits").unwrap_or(stats.prefix_hits);
718        let prefix_misses =
719            engine_u64(engine_prefix_cache, "misses").unwrap_or(stats.prefix_misses);
720        let prefix_evictions =
721            engine_u64(engine_prefix_cache, "evictions").unwrap_or(stats.prefix_evictions);
722        let prefix_saved = engine_u64(engine_prefix_cache, "saved_prefill_tokens")
723            .unwrap_or(stats.prefix_saved_prefill_tokens);
724        let prefix_entries =
725            engine_u64(engine_prefix_cache, "entries").unwrap_or(stats.prefix_entries);
726        let prefix_bytes = engine_u64(engine_prefix_cache, "bytes").unwrap_or(stats.prefix_bytes);
727        let mut prefix_cache = serde_json::json!({
728            "enabled": engine_bool(engine_prefix_cache, "enabled").unwrap_or(policy.prefix_cache_enabled),
729            "position": engine_str(engine_prefix_cache, "position").unwrap_or("product-observability"),
730            "source": engine_str(engine_prefix_cache, "source").unwrap_or("server-prompt-lcp-observability"),
731            "entries": prefix_entries,
732            "hits": prefix_hits,
733            "misses": prefix_misses,
734            "evictions": prefix_evictions,
735            "saved_prefill_tokens": prefix_saved,
736            "bytes": prefix_bytes,
737            "block_size": engine_u64(engine_prefix_cache, "block_size"),
738            "kv_dtype": engine_str(engine_prefix_cache, "kv_dtype"),
739        });
740        if let (Some(engine), Some(prefix)) = (
741            engine_prefix_cache.and_then(|value| value.as_object()),
742            prefix_cache.as_object_mut(),
743        ) {
744            for (key, value) in engine {
745                prefix.entry(key.clone()).or_insert_with(|| value.clone());
746            }
747        }
748        serde_json::json!({
749            "prefix_cache": prefix_cache,
750            "session_cache": {
751                "mode": policy.session_cache_mode,
752                "entries": stats.session_entries,
753                "hits": stats.session_hits,
754                "misses": stats.session_misses,
755                "evictions": stats.session_evictions,
756                "tokens": stats.session_tokens,
757                "max_entries": policy.session_cache_max_entries,
758                "max_tokens": policy.session_cache_max_tokens,
759            }
760        })
761    }
762
763    fn prometheus_metrics(&self, engine_prefix_cache: Option<&serde_json::Value>) -> String {
764        let stats = self.stats();
765        let prefix_hits = engine_u64(engine_prefix_cache, "hits").unwrap_or(stats.prefix_hits);
766        let prefix_misses =
767            engine_u64(engine_prefix_cache, "misses").unwrap_or(stats.prefix_misses);
768        let prefix_evictions =
769            engine_u64(engine_prefix_cache, "evictions").unwrap_or(stats.prefix_evictions);
770        let prefix_saved = engine_u64(engine_prefix_cache, "saved_prefill_tokens")
771            .unwrap_or(stats.prefix_saved_prefill_tokens);
772        let prefix_entries =
773            engine_u64(engine_prefix_cache, "entries").unwrap_or(stats.prefix_entries);
774        let prefix_bytes = engine_u64(engine_prefix_cache, "bytes").unwrap_or(stats.prefix_bytes);
775        format!(
776            concat!(
777                "ferrum_prefix_cache_hits_total {}\n",
778                "ferrum_prefix_cache_misses_total {}\n",
779                "ferrum_prefix_cache_evictions_total {}\n",
780                "ferrum_prefix_cache_saved_prefill_tokens_total {}\n",
781                "ferrum_prefix_cache_entries {}\n",
782                "ferrum_prefix_cache_bytes {}\n",
783                "ferrum_session_cache_hits_total {}\n",
784                "ferrum_session_cache_misses_total {}\n",
785                "ferrum_session_cache_evictions_total {}\n",
786                "ferrum_session_cache_entries {}\n",
787                "ferrum_session_cache_tokens {}\n"
788            ),
789            prefix_hits,
790            prefix_misses,
791            prefix_evictions,
792            prefix_saved,
793            prefix_entries,
794            prefix_bytes,
795            stats.session_hits,
796            stats.session_misses,
797            stats.session_evictions,
798            stats.session_entries,
799            stats.session_tokens,
800        )
801    }
802}
803
804fn engine_u64(snapshot: Option<&serde_json::Value>, key: &str) -> Option<u64> {
805    snapshot?.get(key)?.as_u64()
806}
807
808fn engine_bool(snapshot: Option<&serde_json::Value>, key: &str) -> Option<bool> {
809    snapshot?.get(key)?.as_bool()
810}
811
812fn engine_str<'a>(snapshot: Option<&'a serde_json::Value>, key: &str) -> Option<&'a str> {
813    snapshot?.get(key)?.as_str()
814}
815
816fn auto_config_health_value(auto_config: Option<&ResolvedFerrumConfig>) -> serde_json::Value {
817    match auto_config {
818        Some(auto_config) => auto_config.effective_config_document(),
819        None => {
820            match FerrumConfigBuilder::new(RuntimeConfigSnapshot::capture_current()).resolve() {
821                Ok(auto_config) => auto_config.effective_config_document(),
822                Err(err) => serde_json::json!({
823                    "schema_version": 1,
824                    "error": err.to_string(),
825                }),
826            }
827        }
828    }
829}
830
831fn admission_health_json(
832    engine_status: &EngineStatus,
833    scheduler_metrics: &EngineMetrics,
834    auto_config: &serde_json::Value,
835    runtime_snapshot: Option<&ferrum_types::ExecutorAdmissionSnapshot>,
836    runtime_error: Option<&str>,
837) -> serde_json::Value {
838    let configured = auto_config
839        .get("admission")
840        .and_then(|value| value.as_object());
841    let preflight_effective_max_concurrent = configured
842        .and_then(|value| value.get("effective_max_concurrent"))
843        .and_then(|value| value.as_u64());
844    let effective_max_concurrent = if runtime_error.is_some() {
845        None
846    } else {
847        Some(
848            runtime_snapshot
849                .map(|snapshot| u64::from(snapshot.maximum_active_sequences()))
850                .or(preflight_effective_max_concurrent)
851                .unwrap_or_else(|| {
852                    (engine_status.active_requests + engine_status.queued_requests)
853                        .max(1)
854                        .try_into()
855                        .unwrap_or(u64::MAX)
856                }),
857        )
858    };
859    let active_sequences = runtime_error.is_none().then(|| {
860        runtime_snapshot
861            .map(|snapshot| u64::from(snapshot.active_sequences()))
862            .unwrap_or_else(|| engine_status.active_requests as u64)
863    });
864    let waiting_requests = runtime_error.is_none().then(|| {
865        runtime_snapshot
866            .map(|snapshot| u64::from(snapshot.waiting_requests()))
867            .unwrap_or_else(|| engine_status.queued_requests as u64)
868    });
869    serde_json::json!({
870        "schema_version": 2,
871        "source": if runtime_error.is_some() {
872            "runtime_error"
873        } else if runtime_snapshot.is_some() {
874            "runtime_executor"
875        } else {
876            "startup_preflight_and_engine_status"
877        },
878        "runtime_snapshot_available": runtime_snapshot.is_some(),
879        "runtime_contract_error": runtime_error,
880        "resource_authority": runtime_snapshot
881            .and_then(|snapshot| serde_json::to_value(snapshot.resource_authority()).ok())
882            .unwrap_or(serde_json::Value::Null),
883        "effective_max_concurrent": effective_max_concurrent,
884        "maximum_active_sequences": runtime_snapshot
885            .map(|snapshot| u64::from(snapshot.maximum_active_sequences())),
886        "maximum_scheduled_tokens": runtime_snapshot
887            .map(|snapshot| snapshot.maximum_scheduled_tokens()),
888        "preflight_effective_max_concurrent": preflight_effective_max_concurrent,
889        "queue_depth": waiting_requests,
890        "active_sequences": active_sequences,
891        "active_prefill": runtime_snapshot
892            .map(|snapshot| u64::from(snapshot.active_prefill_sequences())),
893        "active_decode": runtime_snapshot
894            .map(|snapshot| u64::from(snapshot.active_decode_sequences())),
895        "current_batch_size": runtime_snapshot
896            .and_then(|snapshot| snapshot.current_batch_size())
897            .map(u64::from),
898        "capacity_blocked_requests": runtime_snapshot
899            .and_then(|snapshot| snapshot.capacity_blocked_requests())
900            .map(u64::from),
901        "rejected_requests_total": 0u64,
902        "failed_requests_total": scheduler_metrics.failed_requests,
903        "completed_requests_total": scheduler_metrics.successful_requests,
904        "avg_queue_wait_time_ms": scheduler_metrics.queue_metrics.avg_queue_wait_time_ms,
905        "scheduler_policy": configured
906            .and_then(|value| value.get("scheduler_policy"))
907            .and_then(|value| value.as_str())
908            .unwrap_or("unknown"),
909        "phase_detail_source": if runtime_snapshot.is_some() {
910            "scheduler_request_index_single_read"
911        } else {
912            "unavailable"
913        },
914    })
915}
916
917fn admission_prometheus_metrics(admission: &serde_json::Value) -> String {
918    let snapshot_available = u8::from(
919        admission
920            .get("runtime_snapshot_available")
921            .and_then(serde_json::Value::as_bool)
922            .unwrap_or(false),
923    );
924    let mut output = format!("ferrum_admission_runtime_snapshot_available {snapshot_available}\n");
925    for (field, metric) in [
926        (
927            "effective_max_concurrent",
928            "ferrum_admission_effective_max_concurrent",
929        ),
930        (
931            "maximum_active_sequences",
932            "ferrum_admission_maximum_active_sequences",
933        ),
934        (
935            "maximum_scheduled_tokens",
936            "ferrum_admission_maximum_scheduled_tokens",
937        ),
938        ("queue_depth", "ferrum_admission_queue_depth"),
939        (
940            "capacity_blocked_requests",
941            "ferrum_admission_capacity_blocked_requests",
942        ),
943        ("active_sequences", "ferrum_admission_active_sequences"),
944        ("active_prefill", "ferrum_admission_active_prefill"),
945        ("active_decode", "ferrum_admission_active_decode"),
946        ("current_batch_size", "ferrum_admission_current_batch_size"),
947        (
948            "rejected_requests_total",
949            "ferrum_admission_rejected_requests_total",
950        ),
951        (
952            "failed_requests_total",
953            "ferrum_admission_failed_requests_total",
954        ),
955        (
956            "completed_requests_total",
957            "ferrum_admission_completed_requests_total",
958        ),
959    ] {
960        if let Some(value) = admission.get(field).and_then(serde_json::Value::as_u64) {
961            output.push_str(&format!("{metric} {value}\n"));
962        }
963    }
964    output
965}
966
967fn request_session_id(headers: &HeaderMap, request: &ChatCompletionsRequest) -> Option<String> {
968    headers
969        .get(FERRUM_SESSION_HEADER)
970        .and_then(|value| value.to_str().ok())
971        .map(str::trim)
972        .filter(|value| !value.is_empty())
973        .map(str::to_string)
974        .or_else(|| {
975            request
976                .metadata
977                .as_ref()
978                .and_then(|metadata| metadata.get("ferrum_session_id"))
979                .and_then(|value| value.as_str())
980                .map(str::trim)
981                .filter(|value| !value.is_empty())
982                .map(str::to_string)
983        })
984}
985
986fn benchmark_request_correlation(
987    headers: &HeaderMap,
988) -> std::result::Result<Option<BenchmarkRequestCorrelation>, ServerError> {
989    let header_value = |name: &'static str| {
990        headers
991            .get(name)
992            .map(|value| {
993                value.to_str().map_err(|_| {
994                    ServerError::invalid_request(
995                        format!("{name} must contain visible ASCII text"),
996                        Some(name),
997                    )
998                })
999            })
1000            .transpose()
1001    };
1002    BenchmarkRequestCorrelation::from_header_values(
1003        header_value(BENCHMARK_RUN_ID_HEADER)?,
1004        header_value(BENCHMARK_CELL_ID_HEADER)?,
1005        header_value(BENCHMARK_REPEAT_INDEX_HEADER)?,
1006        header_value(BENCHMARK_PHASE_HEADER)?,
1007        header_value(BENCHMARK_REQUEST_INDEX_HEADER)?,
1008    )
1009    .map_err(|error| ServerError::invalid_request(error, Some(BENCHMARK_RUN_ID_HEADER)))
1010}
1011
1012fn extend_benchmark_profile_attributes(
1013    attributes: &mut BTreeMap<String, serde_json::Value>,
1014    correlation: Option<&BenchmarkRequestCorrelation>,
1015) {
1016    let Some(correlation) = correlation else {
1017        return;
1018    };
1019    attributes.extend([
1020        (
1021            "benchmark_run_id".to_string(),
1022            serde_json::json!(correlation.benchmark_run_id),
1023        ),
1024        (
1025            "cell_id".to_string(),
1026            serde_json::json!(correlation.cell_id),
1027        ),
1028        (
1029            "repeat_index".to_string(),
1030            serde_json::json!(correlation.repeat_index),
1031        ),
1032        (
1033            "phase".to_string(),
1034            serde_json::json!(correlation.phase.as_str()),
1035        ),
1036        (
1037            "request_index".to_string(),
1038            serde_json::json!(correlation.request_index),
1039        ),
1040    ]);
1041}
1042
1043fn approx_tokens(text: &str) -> usize {
1044    approx_tokens_for_chars(text.chars().count())
1045}
1046
1047fn approx_tokens_for_chars(chars: usize) -> usize {
1048    (chars / 4).max(1)
1049}
1050
1051fn longest_common_prefix_chars(a: &str, b: &str) -> usize {
1052    a.chars().zip(b.chars()).take_while(|(a, b)| a == b).count()
1053}
1054
1055fn trim_messages_to_token_budget(messages: &mut Vec<ChatMessage>, max_tokens: usize) {
1056    let max_tokens = max_tokens.max(1);
1057    while messages.len() > 1
1058        && messages
1059            .iter()
1060            .map(|msg| approx_tokens(&msg.content))
1061            .sum::<usize>()
1062            > max_tokens
1063    {
1064        messages.remove(0);
1065    }
1066}
1067
1068#[async_trait]
1069impl HttpServer for AxumServer {
1070    async fn start(&self, config: &ServerConfig) -> ferrum_types::Result<()> {
1071        if self.lifecycle.shutdown_requested.load(Ordering::Acquire) {
1072            return Err(Error::internal(
1073                "cannot start Axum server after shutdown was requested",
1074            ));
1075        }
1076        self.lifecycle
1077            .running
1078            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1079            .map_err(|_| Error::internal("Axum server is already running"))?;
1080        let _run_guard = AxumServerRunGuard {
1081            lifecycle: Arc::clone(&self.lifecycle),
1082        };
1083        let addr = format!("{}:{}", config.host, config.port);
1084        info!("Starting Axum server on {}", addr);
1085
1086        let app = self.build_router_with_state(
1087            self.state
1088                .clone()
1089                .with_request_dump_dir(config.request_dump_dir.clone())
1090                .with_profile_jsonl(config.profile_jsonl.clone())
1091                .with_profile_detail(config.profile_detail)
1092                .with_memory_profile_jsonl(config.memory_profile_jsonl.clone()),
1093        );
1094        let listener = tokio::net::TcpListener::bind(&addr)
1095            .await
1096            .map_err(|e| Error::internal(format!("Failed to bind to {}: {}", addr, e)))?;
1097
1098        info!("Server listening on {}", addr);
1099
1100        let lifecycle = Arc::clone(&self.lifecycle);
1101        axum::serve(listener, app)
1102            .with_graceful_shutdown(async move { lifecycle.wait_for_shutdown().await })
1103            .await
1104            .map_err(|e| Error::internal(format!("Server error: {}", e)))?;
1105
1106        Ok(())
1107    }
1108
1109    async fn stop(&self, timeout: std::time::Duration) -> ferrum_types::Result<()> {
1110        let _stop_guard = self.lifecycle.stop_lock.lock().await;
1111        info!("Stopping Axum server");
1112        self.lifecycle.request_shutdown();
1113
1114        let mut first_error = None;
1115        if self.lifecycle.running.load(Ordering::Acquire) {
1116            if tokio::time::timeout(timeout, self.lifecycle.wait_until_stopped())
1117                .await
1118                .is_err()
1119            {
1120                first_error = Some(Error::internal(format!(
1121                    "Axum server did not drain within {} ms",
1122                    timeout.as_millis()
1123                )));
1124            }
1125        }
1126
1127        if !self.lifecycle.engines_stopped.load(Ordering::Acquire) {
1128            match tokio::time::timeout(timeout, self.shutdown_loaded_engines()).await {
1129                Ok(Ok(())) => {
1130                    self.lifecycle
1131                        .engines_stopped
1132                        .store(true, Ordering::Release);
1133                }
1134                Ok(Err(error)) => {
1135                    if first_error.is_none() {
1136                        first_error = Some(error);
1137                    }
1138                }
1139                Err(_) => {
1140                    if first_error.is_none() {
1141                        first_error = Some(Error::internal(format!(
1142                            "engine shutdown did not complete within {} ms",
1143                            timeout.as_millis()
1144                        )));
1145                    }
1146                }
1147            }
1148        }
1149
1150        first_error.map_or(Ok(()), Err)
1151    }
1152
1153    fn is_running(&self) -> bool {
1154        self.lifecycle.running.load(Ordering::Acquire)
1155    }
1156
1157    fn address(&self) -> Option<std::net::SocketAddr> {
1158        // For MVP, return configured address
1159        format!("{}:{}", self.config.host, self.config.port)
1160            .parse()
1161            .ok()
1162    }
1163
1164    fn register_handler(
1165        &mut self,
1166        _path: &str,
1167        _method: HttpMethod,
1168        _handler: Box<dyn crate::traits::RequestHandler>,
1169    ) {
1170        // For MVP, routes are static
1171        unimplemented!("Dynamic handler registration not implemented in MVP")
1172    }
1173
1174    fn register_middleware(&mut self, _middleware: Box<dyn crate::traits::Middleware>) {
1175        // For MVP, middleware is static
1176        unimplemented!("Dynamic middleware registration not implemented in MVP")
1177    }
1178
1179    fn get_metrics(&self) -> ServerMetrics {
1180        // Return empty metrics for MVP
1181        ServerMetrics {
1182            total_requests: 0,
1183            requests_by_endpoint: std::collections::HashMap::new(),
1184            requests_by_status: std::collections::HashMap::new(),
1185            avg_response_time_ms: 0.0,
1186            p95_response_time_ms: 0.0,
1187            p99_response_time_ms: 0.0,
1188            active_connections: 0,
1189            bytes_sent: 0,
1190            bytes_received: 0,
1191            error_rate: 0.0,
1192            uptime_seconds: 0,
1193        }
1194    }
1195
1196    async fn health_check(&self) -> HealthStatus {
1197        HealthStatus::Healthy
1198    }
1199}
1200
1201/// Main chat completions handler
1202async fn chat_completions_handler(
1203    State(state): State<AppState>,
1204    headers: HeaderMap,
1205    request: std::result::Result<Json<ChatCompletionsRequest>, JsonRejection>,
1206) -> std::result::Result<Response, ServerError> {
1207    chat_completions_handler_with_phases(State(state), headers, request, None).await
1208}
1209
1210async fn chat_completions_handler_with_phases(
1211    State(state): State<AppState>,
1212    headers: HeaderMap,
1213    request: std::result::Result<Json<ChatCompletionsRequest>, JsonRejection>,
1214    mut message_phases: Option<Vec<Option<AssistantMessagePhase>>>,
1215) -> std::result::Result<Response, ServerError> {
1216    let Json(mut request) = request.map_err(|error| {
1217        ServerError::invalid_request(
1218            format!(
1219                "invalid chat completions request: {}",
1220                json_rejection_detail(&error)
1221            ),
1222            None,
1223        )
1224    })?;
1225    let benchmark_correlation = benchmark_request_correlation(&headers)?;
1226    let cache_policy = CachePolicy::current();
1227    if message_phases
1228        .as_ref()
1229        .is_some_and(|phases| phases.len() != request.messages.len())
1230    {
1231        return Err(ServerError::InternalError(
1232            "Responses message phase metadata did not match input history".to_string(),
1233        ));
1234    }
1235    let session_context =
1236        state
1237            .cache
1238            .prepare_session_request(&mut request, &headers, &cache_policy);
1239    if let Some(phases) = &mut message_phases {
1240        let prepended = request
1241            .messages
1242            .len()
1243            .checked_sub(phases.len())
1244            .ok_or_else(|| {
1245                ServerError::InternalError(
1246                    "session preparation shortened Responses input history".to_string(),
1247                )
1248            })?;
1249        phases.splice(0..0, std::iter::repeat(None).take(prepended));
1250    }
1251
1252    let span = span!(Level::INFO, "chat_completions", model = %request.model);
1253    let _enter = span.enter();
1254
1255    info!(
1256        "Received chat completions request for model: {}",
1257        request.model
1258    );
1259    debug!("Request: {:?}", request);
1260
1261    // OpenAI spec requires at least one message. Reject empty arrays at
1262    // the boundary rather than synthesising a fake prompt downstream.
1263    validate_chat_request(&request)?;
1264    let (engine_model_id, lora_adapter) = resolve_request_model(
1265        &state.served_model_registry,
1266        &request.model,
1267        ServedModelKind::Llm,
1268    )?;
1269
1270    // Convert OpenAI request to internal format
1271    let mut inference_request = convert_chat_request_with_template_model_and_default(
1272        &request,
1273        &engine_model_id.0,
1274        state.prompt_template.as_deref(),
1275        state.default_enable_thinking,
1276        state.interleaved_system_coalescing.unwrap_or(true),
1277        message_phases.as_deref(),
1278    )
1279    .map_err(server_error_from_ferrum_error)?;
1280    apply_served_model_resolution(&mut inference_request, engine_model_id, lora_adapter);
1281    if state.request_dump_dir.is_some() {
1282        inference_request.evidence_request.capture_prompt_token_ids = true;
1283    }
1284    inference_request
1285        .evidence_request
1286        .capture_engine_token_timing = state.profile_detail.captures_engine_token_timing();
1287    state.record_prefix_prompt(&inference_request.prompt, &cache_policy);
1288    if let Err(err) =
1289        write_chat_request_replay_bundle(&state, &headers, &request, &inference_request)
1290    {
1291        warn!("failed to write chat request replay bundle: {}", err);
1292    }
1293
1294    // Check if streaming is requested
1295    if request.stream.unwrap_or(false) {
1296        handle_chat_completions_stream(state, request, inference_request, benchmark_correlation)
1297            .await
1298    } else {
1299        handle_chat_completions_sync(
1300            state,
1301            request,
1302            inference_request,
1303            session_context,
1304            benchmark_correlation,
1305        )
1306        .await
1307    }
1308}
1309
1310fn json_rejection_detail(rejection: &JsonRejection) -> String {
1311    const MAX_DETAIL_CHARS: usize = 512;
1312
1313    let mut details = Vec::new();
1314    let mut current: Option<&(dyn StdError + 'static)> = Some(rejection);
1315    while let Some(error) = current {
1316        let detail = error.to_string();
1317        if !detail.is_empty() && details.last() != Some(&detail) {
1318            details.push(detail);
1319        }
1320        current = error.source();
1321    }
1322
1323    details.join(": ").chars().take(MAX_DETAIL_CHARS).collect()
1324}
1325
1326fn write_chat_request_replay_bundle(
1327    state: &AppState,
1328    headers: &HeaderMap,
1329    openai_request: &ChatCompletionsRequest,
1330    inference_request: &InferenceRequest,
1331) -> std::result::Result<(), String> {
1332    let Some(root) = state.request_dump_dir.as_ref() else {
1333        return Ok(());
1334    };
1335    let request_id = inference_request.id.to_string();
1336    let bundle_dir = root.join(&request_id);
1337    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1338
1339    let sanitized_body = sanitized_chat_request_body(openai_request);
1340    let replay_body_path = bundle_dir.join("replay_body.json");
1341    write_json_value(&replay_body_path, &sanitized_body)?;
1342    let engine_replay_argv = replay_bundle_argv(&bundle_dir);
1343    let output_text_body = format!(
1344        "[server request replay emitted before response]\nsha256={}\nchars=0\n",
1345        sha256_hex(b"")
1346    );
1347
1348    let request = serde_json::json!({
1349        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1350        "entrypoint": "serve",
1351        "request_id": request_id,
1352        "model": openai_request.model.clone(),
1353        "backend": "actual",
1354        "endpoint": "/v1/chat/completions",
1355        "method": "POST",
1356        "stream": openai_request.stream.unwrap_or(false),
1357        "actual_model_smoke": true,
1358        "sanitized": true,
1359        "http": {
1360            "method": "POST",
1361            "path": "/v1/chat/completions",
1362            "headers": sanitized_replay_headers(headers),
1363            "body": sanitized_body
1364        }
1365    });
1366    let files = [
1367        ("request.json", request),
1368        (
1369            "prompt_token_ids.json",
1370            serde_json::json!({
1371                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1372                "request_id": request_id,
1373                "model": openai_request.model.clone(),
1374                "tokenizer_or_model": openai_request.model.clone(),
1375                "token_ids": null,
1376                "token_count": null,
1377                "unavailable_reason": "server request replay captures the OpenAI body before prompt token ids are retained",
1378                "sanitized": true
1379            }),
1380        ),
1381        (
1382            "sampling_params.json",
1383            serde_json::json!({
1384                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1385                "request_id": request_id,
1386                "sampling_params": inference_request.sampling_params.clone(),
1387                "unavailable_reason": null
1388            }),
1389        ),
1390        (
1391            "runtime_effective_config.json",
1392            serde_json::json!({
1393                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1394                "request_id": request_id,
1395                "entrypoint": "serve",
1396                "endpoint": "/v1/chat/completions",
1397                "stream": openai_request.stream.unwrap_or(false),
1398                "request_dump_dir": root.to_string_lossy(),
1399                "sanitized": true
1400            }),
1401        ),
1402        (
1403            "backend_selection.json",
1404            serde_json::json!({
1405                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1406                "request_id": request_id,
1407                "backend": "actual",
1408                "model": openai_request.model.clone(),
1409                "actual_model_smoke": true
1410            }),
1411        ),
1412        (
1413            "output_token_ids.json",
1414            serde_json::json!({
1415                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1416                "request_id": request_id,
1417                "token_ids": [],
1418                "token_count": 0,
1419                "finish_reason": null,
1420                "unavailable_reason": "server request replay bundle is emitted at request admission in this WP9 slice"
1421            }),
1422        ),
1423        (
1424            "bad_output_scan.json",
1425            serde_json::json!({
1426                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1427                "request_id": request_id,
1428                "bad_output": false,
1429                "bad_text_count": 0,
1430                "reasons": [],
1431                "first_bad_text_span": null,
1432                "failure_kind": null,
1433                "output_chars": 0,
1434                "classified_output_sha256": sha256_hex(b""),
1435                "output_sha256": sha256_hex(output_text_body.as_bytes())
1436            }),
1437        ),
1438        (
1439            "replay.command.json",
1440            serde_json::json!({
1441                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1442                "request_id": request_id,
1443                "entrypoint": "serve",
1444                "command": replay_curl_command(&bundle_dir),
1445                "argv": replay_curl_argv(&bundle_dir),
1446                "bundle_dir": bundle_dir.to_string_lossy(),
1447                "requires_running_server": true,
1448                "engine_replay": {
1449                    "mode": "bundle_offline",
1450                    "requires_http_server": false,
1451                    "command": shell_command(&engine_replay_argv),
1452                    "argv": engine_replay_argv
1453                },
1454                "sanitized": true
1455            }),
1456        ),
1457    ];
1458    for (name, value) in files {
1459        write_json_value(&bundle_dir.join(name), &value)?;
1460    }
1461    fs::write(bundle_dir.join("output_text.txt"), output_text_body)
1462        .map_err(|err| err.to_string())?;
1463    Ok(())
1464}
1465
1466fn write_chat_request_failure_diagnostics(
1467    state: &AppState,
1468    request_id: &str,
1469    failure_kind: &str,
1470    phase: &str,
1471    error_kind: &str,
1472    message: &str,
1473    engine_status: Option<&EngineStatus>,
1474) -> std::result::Result<(), String> {
1475    let admission_summary = state
1476        .auto_config
1477        .as_ref()
1478        .map(|config| config.admission_summary_document());
1479    write_chat_request_failure_diagnostics_at_root(
1480        state.request_dump_dir.as_ref().map(|root| root.as_path()),
1481        admission_summary.as_ref(),
1482        engine_status,
1483        request_id,
1484        failure_kind,
1485        phase,
1486        error_kind,
1487        message,
1488    )
1489}
1490
1491fn write_chat_request_completion_replay_bundle(
1492    request_dump_dir: Option<&Path>,
1493    request_id: &str,
1494    output_text: &str,
1495    output_token_ids: &[TokenId],
1496    finish_reason: Option<&str>,
1497) -> std::result::Result<(), String> {
1498    let Some(root) = request_dump_dir else {
1499        return Ok(());
1500    };
1501    let bundle_dir = root.join(request_id);
1502    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1503    let token_ids = output_token_ids
1504        .iter()
1505        .map(|token| token.get())
1506        .collect::<Vec<_>>();
1507    let output_text_body = format!(
1508        "[redacted actual output]\nsha256={}\nchars={}\n",
1509        sha256_hex(output_text.as_bytes()),
1510        output_text.chars().count()
1511    );
1512    write_json_value(
1513        &bundle_dir.join("output_token_ids.json"),
1514        &serde_json::json!({
1515            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1516            "request_id": request_id,
1517            "token_ids": token_ids,
1518            "token_count": output_token_ids.len(),
1519            "finish_reason": finish_reason,
1520            "unavailable_reason": null
1521        }),
1522    )?;
1523    write_json_value(
1524        &bundle_dir.join("bad_output_scan.json"),
1525        &bad_output_scan_json(request_id, output_text, None, output_text_body.as_bytes()),
1526    )?;
1527    fs::write(bundle_dir.join("output_text.txt"), output_text_body)
1528        .map_err(|err| err.to_string())?;
1529    Ok(())
1530}
1531
1532fn write_chat_prompt_token_evidence(
1533    request_dump_dir: Option<&Path>,
1534    request_id: &str,
1535    model: &str,
1536    execution_evidence: Option<&InferenceExecutionEvidence>,
1537) -> std::result::Result<(), String> {
1538    let (Some(root), Some(evidence)) = (request_dump_dir, execution_evidence) else {
1539        return Ok(());
1540    };
1541    let bundle_dir = root.join(request_id);
1542    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1543    let prompt_token_ids = evidence
1544        .prompt_token_ids
1545        .iter()
1546        .map(|token| token.get())
1547        .collect::<Vec<_>>();
1548    write_json_value(
1549        &bundle_dir.join("prompt_token_ids.json"),
1550        &serde_json::json!({
1551            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1552            "request_id": request_id,
1553            "model": model,
1554            "tokenizer_or_model": model,
1555            "token_ids": prompt_token_ids,
1556            "token_count": evidence.prompt_token_ids.len(),
1557            "unavailable_reason": null,
1558            "sanitized": true
1559        }),
1560    )
1561}
1562
1563#[derive(Clone, Copy, Default)]
1564struct ChatRequestProfileTiming<'a> {
1565    engine_evidence: Option<&'a InferenceExecutionEvidence>,
1566    first_engine_chunk_received_us: Option<u64>,
1567    first_sse_enqueue_us: Option<u64>,
1568}
1569
1570#[allow(clippy::too_many_arguments)]
1571fn write_chat_request_profile_event(
1572    state: &AppState,
1573    request_id: &str,
1574    benchmark_correlation: Option<&BenchmarkRequestCorrelation>,
1575    model: &str,
1576    stream: bool,
1577    phase: &str,
1578    started_at: Instant,
1579    timing: ChatRequestProfileTiming<'_>,
1580    output_token_count: usize,
1581    usage: Option<&TokenUsage>,
1582    finish_reason: Option<&str>,
1583    error: Option<ProfileError>,
1584) -> std::result::Result<(), String> {
1585    let Some(path) = state.profile_jsonl.as_ref() else {
1586        return Ok(());
1587    };
1588    let timestamp = chrono::Utc::now();
1589    let status = if error.is_some() {
1590        ProfileStatus::Failure
1591    } else {
1592        ProfileStatus::Ok
1593    };
1594    let duration_us = elapsed_us_since(started_at);
1595    let mut attributes = BTreeMap::from([
1596        ("actual_model_smoke".to_string(), serde_json::json!(true)),
1597        (
1598            "diagnostic_only".to_string(),
1599            serde_json::json!(state.profile_detail.diagnostic_only()),
1600        ),
1601        (
1602            "endpoint".to_string(),
1603            serde_json::json!("/v1/chat/completions"),
1604        ),
1605        (
1606            "e2e_duration_us".to_string(),
1607            serde_json::json!(duration_us),
1608        ),
1609        ("l0_only".to_string(), serde_json::json!(false)),
1610        (
1611            "profile_detail".to_string(),
1612            serde_json::json!(state.profile_detail.as_str()),
1613        ),
1614        ("stream".to_string(), serde_json::json!(stream)),
1615        (
1616            "output_token_count".to_string(),
1617            serde_json::json!(output_token_count),
1618        ),
1619        (
1620            "execution_request_id".to_string(),
1621            serde_json::json!(format!("request.product.{request_id}")),
1622        ),
1623    ]);
1624    extend_benchmark_profile_attributes(&mut attributes, benchmark_correlation);
1625    if let Some(usage) = usage {
1626        attributes.insert(
1627            "prompt_token_count".to_string(),
1628            serde_json::json!(usage.prompt_tokens),
1629        );
1630        attributes.insert(
1631            "completion_token_count".to_string(),
1632            serde_json::json!(usage.completion_tokens),
1633        );
1634        attributes.insert(
1635            "total_token_count".to_string(),
1636            serde_json::json!(usage.total_tokens),
1637        );
1638        attributes.insert("token_count_source".to_string(), serde_json::json!("usage"));
1639    } else {
1640        attributes.insert(
1641            "completion_token_count".to_string(),
1642            serde_json::json!(output_token_count),
1643        );
1644        attributes.insert(
1645            "total_token_count".to_string(),
1646            serde_json::json!(output_token_count),
1647        );
1648        attributes.insert(
1649            "token_count_source".to_string(),
1650            serde_json::json!("generated_tokens"),
1651        );
1652    }
1653    if let Some(engine_timing) = timing
1654        .engine_evidence
1655        .and_then(|evidence| evidence.engine_token_timing.as_ref())
1656    {
1657        engine_timing
1658            .validate(output_token_count)
1659            .map_err(|error| format!("invalid engine token timing evidence: {error}"))?;
1660        attributes.extend(ferrum_types::engine_token_timing_profile_attributes(
1661            engine_timing,
1662        ));
1663    } else if status == ProfileStatus::Ok && state.profile_detail.captures_engine_token_timing() {
1664        return Err(format!(
1665            "{} profile completed without required engine token timing evidence",
1666            state.profile_detail.as_str()
1667        ));
1668    }
1669    if let Some(received_us) = timing.first_engine_chunk_received_us {
1670        attributes.insert(
1671            "engine_stream_first_chunk_received_us".to_string(),
1672            serde_json::json!(received_us),
1673        );
1674    }
1675    if let Some(enqueue_us) = timing.first_sse_enqueue_us {
1676        attributes.insert(
1677            "http_first_sse_enqueue_us".to_string(),
1678            serde_json::json!(enqueue_us),
1679        );
1680    }
1681    if stream {
1682        attributes.insert(
1683            "http_stream_flush_unavailable_reason".to_string(),
1684            serde_json::json!(
1685                "socket flush completion is outside the axum handler observation boundary"
1686            ),
1687        );
1688    }
1689    if let Some(reason) = finish_reason {
1690        attributes.insert("finish_reason".to_string(), serde_json::json!(reason));
1691    }
1692    if let Some(error) = error.as_ref() {
1693        attributes.insert(
1694            if error.blocking {
1695                "first_failure_event"
1696            } else {
1697                "terminal_failure_event"
1698            }
1699            .to_string(),
1700            serde_json::json!(true),
1701        );
1702    }
1703
1704    let replay = state.request_dump_dir.as_ref().map(|root| {
1705        let bundle_dir = root.join(request_id);
1706        ReplayReference {
1707            command: replay_curl_command(&bundle_dir),
1708            bundle_dir: Some(root.to_string_lossy().to_string()),
1709        }
1710    });
1711    let resource = error.as_ref().map(|error| ResourceTraceEvent {
1712        owner_kind: "request".to_string(),
1713        owner_id: request_id.to_string(),
1714        resource_kind: "chat_request".to_string(),
1715        action: ResourceAction::Reject,
1716        amount: None,
1717        before: None,
1718        after: None,
1719        capacity: Some(1),
1720        underflow_amount: None,
1721        reason: Some(error.message.clone()),
1722        error_kind: Some(error.kind.clone()),
1723        message: Some(error.message.clone()),
1724        resource_error_kind: Some(error.kind.clone()),
1725    });
1726    let event = FerrumProfileEvent {
1727        schema_version: OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1728        ts_unix_nanos: timestamp
1729            .timestamp_nanos_opt()
1730            .unwrap_or_else(|| timestamp.timestamp_micros() * 1_000),
1731        event_id: format!(
1732            "evt-server-chat-{}-{request_id}",
1733            if stream { "stream" } else { "sync" }
1734        ),
1735        request_id: request_id.to_string(),
1736        correlation_id: Some(request_id.to_string()),
1737        entrypoint: ProfileEntrypoint::Serve,
1738        backend: "actual".to_string(),
1739        runtime_preset_hash: state
1740            .auto_config
1741            .as_ref()
1742            .map(ResolvedFerrumConfig::runtime_env_hash)
1743            .unwrap_or_else(|| format!("sha256:{}", sha256_hex(b"serve-profile"))),
1744        phase: phase.to_string(),
1745        event_kind: ProfileEventKind::TimedSpan,
1746        timestamp,
1747        status,
1748        model: Some(model.to_string()),
1749        duration_us: Some(duration_us),
1750        memory: None,
1751        resource,
1752        error,
1753        replay,
1754        shape: BTreeMap::from([("batch_size".to_string(), serde_json::json!(1))]),
1755        backend_detail: None,
1756        attributes,
1757    };
1758    append_profile_event(path.as_path(), &event)
1759}
1760
1761fn maybe_write_first_request_memory_stage(
1762    state: &AppState,
1763    request_id: &str,
1764    benchmark_correlation: Option<&BenchmarkRequestCorrelation>,
1765    model: &str,
1766    stream: bool,
1767    started_at: Instant,
1768    before: Option<ProcessMemorySample>,
1769) -> std::result::Result<(), String> {
1770    if state.profile_jsonl.is_none() && state.memory_profile_jsonl.is_none() {
1771        return Ok(());
1772    }
1773    if state
1774        .first_request_memory_recorded
1775        .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1776        .is_err()
1777    {
1778        return Ok(());
1779    }
1780    let after = ProcessMemorySampler.sample();
1781    let memory = after.map(|after| ProcessMemoryObservation::from_samples(before, after));
1782    let timestamp = chrono::Utc::now();
1783    let mut attributes = BTreeMap::from([
1784        ("actual_model_smoke".to_string(), serde_json::json!(true)),
1785        (
1786            "diagnostic_only".to_string(),
1787            serde_json::json!(state.profile_detail.diagnostic_only()),
1788        ),
1789        (
1790            "endpoint".to_string(),
1791            serde_json::json!("/v1/chat/completions"),
1792        ),
1793        ("l0_only".to_string(), serde_json::json!(false)),
1794        (
1795            "memory_stage".to_string(),
1796            serde_json::json!("first_request_done"),
1797        ),
1798        (
1799            "profile_detail".to_string(),
1800            serde_json::json!(state.profile_detail.as_str()),
1801        ),
1802        ("stream".to_string(), serde_json::json!(stream)),
1803    ]);
1804    extend_benchmark_profile_attributes(&mut attributes, benchmark_correlation);
1805    let memory_snapshot = if let Some(memory) = &memory {
1806        attributes.insert(
1807            "memory_measurement".to_string(),
1808            serde_json::json!("process_rss"),
1809        );
1810        attributes.insert(
1811            "process_memory_source".to_string(),
1812            serde_json::json!(memory.source),
1813        );
1814        memory.to_snapshot("process", Some("actual"))
1815    } else {
1816        attributes.insert(
1817            "memory_measurement".to_string(),
1818            serde_json::json!("not_collected"),
1819        );
1820        ferrum_types::MemorySnapshot {
1821            scope: "process".to_string(),
1822            backend: Some("actual".to_string()),
1823            before_bytes: Some(0),
1824            after_bytes: Some(0),
1825            current_bytes: Some(0),
1826            high_water_bytes: Some(0),
1827            available_bytes: None,
1828        }
1829    };
1830    let replay = state.request_dump_dir.as_ref().map(|root| {
1831        let bundle_dir = root.join(request_id);
1832        ReplayReference {
1833            command: replay_curl_command(&bundle_dir),
1834            bundle_dir: Some(root.to_string_lossy().to_string()),
1835        }
1836    });
1837    let event = FerrumProfileEvent {
1838        schema_version: OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1839        ts_unix_nanos: timestamp
1840            .timestamp_nanos_opt()
1841            .unwrap_or_else(|| timestamp.timestamp_micros() * 1_000),
1842        event_id: format!("evt-server-chat-memory-first-request-{request_id}"),
1843        request_id: request_id.to_string(),
1844        correlation_id: Some(request_id.to_string()),
1845        entrypoint: ProfileEntrypoint::Serve,
1846        backend: "actual".to_string(),
1847        runtime_preset_hash: state
1848            .auto_config
1849            .as_ref()
1850            .map(ResolvedFerrumConfig::runtime_env_hash)
1851            .unwrap_or_else(|| format!("sha256:{}", sha256_hex(b"serve-profile"))),
1852        phase: "actual_serve_first_request_done".to_string(),
1853        event_kind: ProfileEventKind::Memory,
1854        timestamp,
1855        status: ProfileStatus::Ok,
1856        model: Some(model.to_string()),
1857        duration_us: Some(elapsed_us_since(started_at)),
1858        memory: Some(memory_snapshot),
1859        resource: None,
1860        error: None,
1861        replay,
1862        shape: BTreeMap::from([("batch_size".to_string(), serde_json::json!(1))]),
1863        backend_detail: None,
1864        attributes,
1865    };
1866    if let Some(path) = &state.profile_jsonl {
1867        append_profile_event(path.as_path(), &event)?;
1868    }
1869    if let Some(path) = &state.memory_profile_jsonl {
1870        append_profile_event(path.as_path(), &event)?;
1871    }
1872    Ok(())
1873}
1874
1875fn request_memory_sample_before(state: &AppState) -> Option<ProcessMemorySample> {
1876    (state.profile_jsonl.is_some() || state.memory_profile_jsonl.is_some())
1877        .then(|| ProcessMemorySampler.sample())
1878        .flatten()
1879}
1880
1881fn append_profile_event(
1882    path: &Path,
1883    event: &FerrumProfileEvent,
1884) -> std::result::Result<(), String> {
1885    event.validate().map_err(|err| err.to_string())?;
1886    ferrum_bench_core::write_jsonl_records(
1887        path,
1888        ferrum_bench_core::JsonlJournalOpenMode::Append,
1889        std::slice::from_ref(event),
1890    )
1891    .map_err(|error| error.to_string())
1892}
1893
1894fn elapsed_us_since(started_at: Instant) -> u64 {
1895    started_at
1896        .elapsed()
1897        .as_micros()
1898        .max(1)
1899        .try_into()
1900        .unwrap_or(u64::MAX)
1901}
1902
1903fn write_chat_request_failure_diagnostics_at_root(
1904    request_dump_dir: Option<&Path>,
1905    admission_summary: Option<&serde_json::Value>,
1906    engine_status: Option<&EngineStatus>,
1907    request_id: &str,
1908    failure_kind: &str,
1909    phase: &str,
1910    error_kind: &str,
1911    message: &str,
1912) -> std::result::Result<(), String> {
1913    let Some(root) = request_dump_dir else {
1914        return Ok(());
1915    };
1916    let bundle_dir = root.join(request_id);
1917    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1918    let message = sanitize_diagnostic_text(message);
1919    let now = chrono::Utc::now();
1920
1921    let bad_scan_path = bundle_dir.join("bad_output_scan.json");
1922    let mut bad_scan = fs::read_to_string(&bad_scan_path)
1923        .ok()
1924        .and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
1925        .filter(|value| value.is_object())
1926        .unwrap_or_else(|| serde_json::json!({}));
1927    let bad_scan_obj = bad_scan
1928        .as_object_mut()
1929        .expect("bad scan fallback should be an object");
1930    bad_scan_obj.insert(
1931        "schema_version".to_string(),
1932        serde_json::json!(OBSERVABILITY_PROFILE_SCHEMA_VERSION),
1933    );
1934    bad_scan_obj.insert("request_id".to_string(), serde_json::json!(request_id));
1935    bad_scan_obj
1936        .entry("bad_output".to_string())
1937        .or_insert_with(|| serde_json::json!(false));
1938    bad_scan_obj
1939        .entry("bad_text_count".to_string())
1940        .or_insert_with(|| serde_json::json!(0));
1941    bad_scan_obj
1942        .entry("reasons".to_string())
1943        .or_insert_with(|| serde_json::json!([]));
1944    bad_scan_obj
1945        .entry("first_bad_text_span".to_string())
1946        .or_insert(serde_json::Value::Null);
1947    bad_scan_obj.insert("failure_kind".to_string(), serde_json::json!(failure_kind));
1948    bad_scan_obj.insert("failure_phase".to_string(), serde_json::json!(phase));
1949    bad_scan_obj.insert("error_kind".to_string(), serde_json::json!(error_kind));
1950    bad_scan_obj
1951        .entry("output_chars".to_string())
1952        .or_insert_with(|| serde_json::json!(0));
1953    bad_scan_obj
1954        .entry("output_sha256".to_string())
1955        .or_insert_with(|| serde_json::json!(sha256_hex(b"")));
1956    write_json_value(&bad_scan_path, &bad_scan)?;
1957
1958    let diagnostics = if chat_resource_failure_kind(failure_kind) {
1959        chat_resource_failure_diagnostics(
1960            request_id,
1961            failure_kind,
1962            phase,
1963            error_kind,
1964            &message,
1965            now.timestamp_millis(),
1966            admission_summary,
1967            engine_status,
1968        )
1969    } else {
1970        serde_json::json!({
1971            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1972            "entrypoint": "serve",
1973            "request_id": request_id,
1974            "failure_kind": failure_kind,
1975            "phase": phase,
1976            "first_failure_event": {
1977                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1978                "entrypoint": "serve",
1979                "request_id": request_id,
1980                "phase": phase,
1981                "error_kind": error_kind,
1982                "message": message,
1983                "timestamp_unix_ms": now.timestamp_millis()
1984            },
1985            "nearest_request_id": request_id,
1986            "log_excerpt": format!("{phase}: {message}"),
1987            "backtrace_excerpt": null,
1988            "nearest_resource_event": null,
1989            "nearest_memory_snapshot": null
1990        })
1991    };
1992    write_json_value(&bundle_dir.join("failure_diagnostics.json"), &diagnostics)?;
1993    Ok(())
1994}
1995
1996fn chat_resource_failure_diagnostics(
1997    request_id: &str,
1998    failure_kind: &str,
1999    phase: &str,
2000    error_kind: &str,
2001    message: &str,
2002    timestamp_unix_ms: i64,
2003    admission_summary: Option<&serde_json::Value>,
2004    engine_status: Option<&EngineStatus>,
2005) -> serde_json::Value {
2006    let resource_kind = chat_resource_kind_for_failure(failure_kind);
2007    let memory = engine_status
2008        .map(|status| &status.memory_usage)
2009        .map(|memory| {
2010            let current = memory.used_bytes as i64;
2011            let high_water = current.max(0);
2012            serde_json::json!({
2013                "scope": "serve_failure",
2014                "backend": "engine_status",
2015                "current_bytes": current.max(0),
2016                "high_water_bytes": high_water,
2017                "total_bytes": memory.total_bytes,
2018                "free_bytes": memory.free_bytes,
2019                "gpu_memory_bytes": memory.gpu_memory_bytes,
2020                "cpu_memory_bytes": memory.cpu_memory_bytes,
2021                "source": "engine_status"
2022            })
2023        })
2024        .unwrap_or_else(|| {
2025            serde_json::json!({
2026                "scope": "serve_failure",
2027                "backend": "engine_status",
2028                "current_bytes": 0,
2029                "high_water_bytes": 0,
2030                "source": "not_collected"
2031            })
2032        });
2033    let capacity = chat_failure_capacity(resource_kind, admission_summary, engine_status, message);
2034    let needed = capacity
2035        .get("needed")
2036        .and_then(|value| value.as_i64())
2037        .unwrap_or(1)
2038        .max(1);
2039    let capacity_value = capacity
2040        .get("capacity")
2041        .and_then(|value| value.as_i64())
2042        .unwrap_or(0)
2043        .max(0);
2044    serde_json::json!({
2045        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2046        "entrypoint": "serve",
2047        "request_id": request_id,
2048        "failure_kind": failure_kind,
2049        "phase": phase,
2050        "first_failure_event": {
2051            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2052            "entrypoint": "serve",
2053            "request_id": request_id,
2054            "phase": phase,
2055            "error_kind": error_kind,
2056            "message": message,
2057            "timestamp_unix_ms": timestamp_unix_ms
2058        },
2059        "nearest_request_id": request_id,
2060        "log_excerpt": format!("{phase}: {message}"),
2061        "capacity": capacity,
2062        "nearest_resource_event": {
2063            "owner_kind": "request",
2064            "owner_id": request_id,
2065            "resource_kind": resource_kind,
2066            "action": "reject",
2067            "amount": needed,
2068            "before": capacity_value,
2069            "after": capacity_value,
2070            "capacity": capacity_value,
2071            "reason": message
2072        },
2073        "nearest_memory_snapshot": memory
2074    })
2075}
2076
2077fn chat_failure_capacity(
2078    resource_kind: &str,
2079    admission_summary: Option<&serde_json::Value>,
2080    engine_status: Option<&EngineStatus>,
2081    reason: &str,
2082) -> serde_json::Value {
2083    if resource_kind == "device_memory" {
2084        let (needed, available, capacity) = engine_status
2085            .map(|status| {
2086                let memory = &status.memory_usage;
2087                let used = memory.used_bytes as i64;
2088                let available = memory.free_bytes as i64;
2089                let capacity = memory.total_bytes as i64;
2090                (
2091                    used.saturating_add(1).max(1),
2092                    available.max(0),
2093                    capacity.max(0),
2094                )
2095            })
2096            .unwrap_or((1, 0, 0));
2097        return serde_json::json!({
2098            "resource_kind": resource_kind,
2099            "needed": needed,
2100            "available": available,
2101            "capacity": capacity,
2102            "reason": reason
2103        });
2104    }
2105    let capacity = admission_summary
2106        .and_then(|summary| summary.get("effective_max_concurrent"))
2107        .and_then(|value| {
2108            value
2109                .as_i64()
2110                .or_else(|| value.as_u64().map(|value| value as i64))
2111        })
2112        .unwrap_or_else(|| {
2113            engine_status
2114                .map(|status| {
2115                    (status.active_requests as i64)
2116                        .saturating_add(status.queued_requests as i64)
2117                        .saturating_add(1)
2118                })
2119                .unwrap_or(0)
2120        })
2121        .max(0);
2122    let used = engine_status
2123        .map(|status| (status.active_requests as i64).saturating_add(status.queued_requests as i64))
2124        .unwrap_or(0)
2125        .max(0);
2126    serde_json::json!({
2127        "resource_kind": resource_kind,
2128        "needed": 1,
2129        "available": capacity.saturating_sub(used),
2130        "capacity": capacity,
2131        "reason": reason
2132    })
2133}
2134
2135fn chat_resource_failure_kind(failure_kind: &str) -> bool {
2136    matches!(
2137        failure_kind,
2138        "oom" | "prevented_oom" | "admission" | "admission_reject" | "oom_admission"
2139    )
2140}
2141
2142fn chat_resource_kind_for_failure(failure_kind: &str) -> &'static str {
2143    match failure_kind {
2144        "oom" | "prevented_oom" => "device_memory",
2145        _ => "admission_capacity",
2146    }
2147}
2148
2149fn sanitize_diagnostic_text(message: &str) -> String {
2150    let trimmed = message.trim();
2151    if trimmed.is_empty() {
2152        return "generation failed without an error message".to_string();
2153    }
2154    let lower = trimmed.to_ascii_lowercase();
2155    if lower.contains("authorization")
2156        || lower.contains("cookie")
2157        || lower.contains("api_key")
2158        || lower.contains("access_token")
2159        || lower.contains("refresh_token")
2160        || lower.contains("password")
2161        || trimmed.contains("sk-")
2162    {
2163        return "[redacted diagnostic message]".to_string();
2164    }
2165    trimmed.chars().take(2048).collect()
2166}
2167
2168fn sanitized_replay_headers(headers: &HeaderMap) -> serde_json::Value {
2169    let mut result = serde_json::Map::new();
2170    for key in ["content-type", "traceparent", "tracestate"] {
2171        if let Some(value) = headers.get(key).and_then(|value| value.to_str().ok()) {
2172            result.insert(key.to_string(), serde_json::json!(value));
2173        }
2174    }
2175    result.insert("authorization".to_string(), serde_json::json!("[redacted]"));
2176    result.insert("cookie".to_string(), serde_json::json!("[redacted]"));
2177    serde_json::Value::Object(result)
2178}
2179
2180fn sanitized_chat_request_body(request: &ChatCompletionsRequest) -> serde_json::Value {
2181    let mut value = serde_json::to_value(request).unwrap_or_else(|_| {
2182        serde_json::json!({
2183            "model": request.model.clone(),
2184            "stream": request.stream.unwrap_or(false),
2185            "messages": []
2186        })
2187    });
2188    redact_json_value(&mut value, None);
2189    value
2190}
2191
2192fn redact_json_value(value: &mut serde_json::Value, key: Option<&str>) {
2193    if key.is_some_and(is_secret_key) {
2194        *value = serde_json::json!("[redacted]");
2195        return;
2196    }
2197    if matches!(key, Some("content" | "arguments")) && value.is_string() {
2198        *value = serde_json::json!("[redacted]");
2199        return;
2200    }
2201    match value {
2202        serde_json::Value::Object(map) => {
2203            for field in ["content", "arguments"] {
2204                if let Some(chars) = map
2205                    .get(field)
2206                    .and_then(|child| child.as_str())
2207                    .map(|text| text.chars().count())
2208                {
2209                    map.insert(field.to_string(), serde_json::json!("[redacted]"));
2210                    map.insert(format!("{field}_redacted"), serde_json::json!(true));
2211                    map.insert(format!("{field}_chars"), serde_json::json!(chars));
2212                }
2213            }
2214            for (child_key, child) in map.iter_mut() {
2215                redact_json_value(child, Some(child_key.as_str()));
2216            }
2217        }
2218        serde_json::Value::Array(items) => {
2219            for child in items {
2220                redact_json_value(child, None);
2221            }
2222        }
2223        _ => {}
2224    }
2225}
2226
2227fn is_secret_key(key: &str) -> bool {
2228    let normalized = key
2229        .chars()
2230        .filter(|ch| *ch != '-' && *ch != '_')
2231        .flat_map(char::to_lowercase)
2232        .collect::<String>();
2233    matches!(
2234        normalized.as_str(),
2235        "authorization"
2236            | "cookie"
2237            | "secret"
2238            | "apikey"
2239            | "password"
2240            | "accesstoken"
2241            | "refreshtoken"
2242            | "idtoken"
2243    )
2244}
2245
2246fn replay_curl_argv(bundle_dir: &Path) -> Vec<String> {
2247    vec![
2248        "curl".to_string(),
2249        "-sS".to_string(),
2250        "-X".to_string(),
2251        "POST".to_string(),
2252        "http://127.0.0.1:8000/v1/chat/completions".to_string(),
2253        "-H".to_string(),
2254        "content-type: application/json".to_string(),
2255        "--data-binary".to_string(),
2256        format!("@{}", bundle_dir.join("replay_body.json").display()),
2257    ]
2258}
2259
2260fn replay_curl_command(bundle_dir: &Path) -> String {
2261    shell_command(&replay_curl_argv(bundle_dir))
2262}
2263
2264fn replay_bundle_argv(bundle_dir: &Path) -> Vec<String> {
2265    vec![
2266        "cargo".to_string(),
2267        "run".to_string(),
2268        "-p".to_string(),
2269        "ferrum-cli".to_string(),
2270        "--".to_string(),
2271        "replay-bundle".to_string(),
2272        bundle_dir.to_string_lossy().to_string(),
2273        "--out".to_string(),
2274        bundle_dir
2275            .join("engine_replay")
2276            .to_string_lossy()
2277            .to_string(),
2278        "--json".to_string(),
2279    ]
2280}
2281
2282fn shell_command(argv: &[String]) -> String {
2283    argv.iter()
2284        .map(|part| shell_quote(part))
2285        .collect::<Vec<_>>()
2286        .join(" ")
2287}
2288
2289fn shell_quote(value: &str) -> String {
2290    if value
2291        .chars()
2292        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | ':' | '@'))
2293    {
2294        value.to_string()
2295    } else {
2296        format!("'{}'", value.replace('\'', "'\\''"))
2297    }
2298}
2299
2300fn write_json_value(path: &Path, value: &serde_json::Value) -> std::result::Result<(), String> {
2301    let bytes = serde_json::to_vec_pretty(value).map_err(|err| err.to_string())?;
2302    fs::write(path, [bytes, b"\n".to_vec()].concat()).map_err(|err| err.to_string())
2303}
2304
2305fn bad_output_scan_json(
2306    request_id: &str,
2307    text: &str,
2308    failure_kind: Option<&str>,
2309    output_artifact_bytes: &[u8],
2310) -> serde_json::Value {
2311    let mut reasons = Vec::new();
2312    let mut first_span: Option<serde_json::Value> = None;
2313    for (needle, reason) in [
2314        ("<unk>", "reserved_token"),
2315        ("[PAD", "reserved_token"),
2316        ("<pad>", "reserved_token"),
2317        ("<|endoftext|>", "reserved_token"),
2318        ("<|im_start|>", "reserved_token"),
2319        ("<|im_end|>", "reserved_token"),
2320        ("<|reserved_special_token", "reserved_token"),
2321        ("\u{fffd}", "invalid_utf8"),
2322    ] {
2323        if let Some(index) = text.find(needle) {
2324            reasons.push(reason);
2325            first_span.get_or_insert_with(|| {
2326                serde_json::json!({
2327                    "byte_start": index,
2328                    "byte_end": index + needle.len(),
2329                    "text": needle,
2330                    "reason": reason
2331                })
2332            });
2333        }
2334    }
2335    if let Some(index) = first_mojibake_index(text) {
2336        reasons.push("mojibake");
2337        first_span.get_or_insert_with(|| {
2338            serde_json::json!({
2339                "byte_start": index,
2340                "byte_end": index + 1,
2341                "reason": "mojibake"
2342            })
2343        });
2344    }
2345    reasons.sort_unstable();
2346    reasons.dedup();
2347    let bad_output = !reasons.is_empty();
2348    serde_json::json!({
2349        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2350        "request_id": request_id,
2351        "bad_output": bad_output,
2352        "bad_text_count": if bad_output { 1 } else { 0 },
2353        "reasons": reasons,
2354        "first_bad_text_span": first_span,
2355        "failure_kind": failure_kind,
2356        "output_chars": text.chars().count(),
2357        "classified_output_sha256": sha256_hex(text.as_bytes()),
2358        "output_sha256": sha256_hex(output_artifact_bytes)
2359    })
2360}
2361
2362fn first_mojibake_index(text: &str) -> Option<usize> {
2363    let mut chars = text.char_indices().peekable();
2364    while let Some((index, ch)) = chars.next() {
2365        match ch {
2366            '\u{00c2}' | '\u{00c3}' => {
2367                if chars.peek().is_some_and(|(_, next)| !next.is_ascii()) {
2368                    return Some(index);
2369                }
2370            }
2371            '\u{00e2}' => {
2372                if chars.peek().is_some_and(|(_, next)| *next == '\u{20ac}') {
2373                    return Some(index);
2374                }
2375            }
2376            _ => {}
2377        }
2378    }
2379    None
2380}
2381
2382fn sha256_hex(bytes: &[u8]) -> String {
2383    let mut hasher = Sha256::new();
2384    hasher.update(bytes);
2385    format!("{:x}", hasher.finalize())
2386}
2387
2388struct ParsedChatModelOutput {
2389    visible: ParsedReasoningResponse,
2390    harmony_response: Option<ferrum_types::ApiChatResponse>,
2391}
2392
2393fn parse_chat_model_output(
2394    protocol: ModelOutputProtocol,
2395    text: &str,
2396    started_in_think: bool,
2397    finish_reason: FinishReason,
2398) -> std::result::Result<ParsedChatModelOutput, ServerError> {
2399    match protocol {
2400        ModelOutputProtocol::Text | ModelOutputProtocol::GemmaThought => {
2401            Ok(ParsedChatModelOutput {
2402                visible: parse_model_reasoning_response(protocol, text, started_in_think)
2403                    .map_err(|error| ServerError::InternalError(error.to_string()))?,
2404                harmony_response: None,
2405            })
2406        }
2407        ModelOutputProtocol::HarmonyGptOss => {
2408            let parsed = parse_harmony_response_for_finish_reason(text, Some(finish_reason))
2409                .map_err(|error| {
2410                    ServerError::InternalError(format!(
2411                        "model output did not satisfy the GPT-OSS Harmony protocol: {error}"
2412                    ))
2413                })?;
2414            let harmony_response =
2415                parsed
2416                    .tool_call
2417                    .map(|tool_call| ferrum_types::ApiChatResponse {
2418                        message: ferrum_types::ApiChatMessage {
2419                            role: ferrum_types::ApiMessageRole::Assistant,
2420                            content: String::new(),
2421                            name: None,
2422                            tool_calls: vec![ferrum_types::ApiToolCall {
2423                                id: format!("call_{}", Uuid::new_v4().simple()),
2424                                tool_type: "function".to_string(),
2425                                function: ferrum_types::ApiFunctionCall {
2426                                    name: tool_call.name,
2427                                    arguments: tool_call.arguments_json,
2428                                },
2429                            }],
2430                            tool_call_id: None,
2431                            function_call: None,
2432                        },
2433                        finish_reason: Some("tool_calls".to_string()),
2434                    });
2435            Ok(ParsedChatModelOutput {
2436                visible: ParsedReasoningResponse {
2437                    content: parsed.content,
2438                    reasoning: parsed.reasoning_content,
2439                },
2440                harmony_response,
2441            })
2442        }
2443    }
2444}
2445
2446/// Handle streaming chat completions
2447async fn handle_chat_completions_stream(
2448    state: AppState,
2449    openai_request: ChatCompletionsRequest,
2450    inference_request: InferenceRequest,
2451    benchmark_correlation: Option<BenchmarkRequestCorrelation>,
2452) -> std::result::Result<Response, ServerError> {
2453    let (tx, rx) = mpsc::unbounded_channel::<std::result::Result<Event, axum::Error>>();
2454
2455    // Spawn task to generate tokens
2456    let engine = state.llm.clone().ok_or_else(|| {
2457        ServerError::ServiceUnavailable("LLM engine not loaded; chat unavailable".into())
2458    })?;
2459    let request_id = inference_request.id.to_string();
2460    let include_stream_usage = openai_request
2461        .stream_options
2462        .as_ref()
2463        .and_then(|opts| opts.include_usage)
2464        .unwrap_or(false);
2465    let output_contract = EffectiveChatOutputContract::resolve(&openai_request);
2466    let buffer_json_object_stream = matches!(
2467        output_contract,
2468        EffectiveChatOutputContract::JsonObjectContent
2469    );
2470    let buffer_strict_json_schema_stream = matches!(
2471        output_contract,
2472        EffectiveChatOutputContract::StrictJsonSchemaContent
2473    );
2474    let stream_api_request = match inference_request.api_request.as_ref() {
2475        Some(ferrum_types::ApiRequest::Chat(request)) => request.clone(),
2476        _ => api_chat_request(
2477            &openai_request,
2478            openai_request.tool_choice.as_ref(),
2479            ferrum_types::ApiToolCallProtocol::default(),
2480        ),
2481    };
2482    let buffer_structured_api_stream =
2483        ferrum_types::chat_api_may_emit_tool_or_function_call(&stream_api_request);
2484    let model_output_protocol = inference_request.sampling_params.model_output_protocol;
2485    let buffer_stream_output = buffer_json_object_stream
2486        || buffer_strict_json_schema_stream
2487        || buffer_structured_api_stream
2488        || model_output_protocol == ModelOutputProtocol::HarmonyGptOss;
2489    // R1-distill-style templates open the think block inside the prompt;
2490    // the parser must know generation starts mid-think.
2491    let started_in_think = request_started_in_reasoning(&inference_request);
2492    let mut native_projector = NativeChatOutputProjector::for_request(&inference_request);
2493    let replay_request_id = inference_request.id.to_string();
2494    let profile_request_model = openai_request.model.clone();
2495    let profile_started_at = Instant::now();
2496    let request_memory_before = request_memory_sample_before(&state);
2497    let mut stream = match engine.infer_stream(inference_request).await {
2498        Ok(stream) => stream,
2499        Err(e) => {
2500            let failure_kind = e.observability_failure_kind();
2501            let error_kind = e.observability_error_kind();
2502            let error_message = e.to_string();
2503            if let Err(err) = write_chat_request_profile_event(
2504                &state,
2505                &replay_request_id,
2506                benchmark_correlation.as_ref(),
2507                &profile_request_model,
2508                true,
2509                "chat_completions_stream_start",
2510                profile_started_at,
2511                ChatRequestProfileTiming::default(),
2512                0,
2513                None,
2514                Some("error"),
2515                Some(ProfileError {
2516                    kind: error_kind.to_string(),
2517                    message: error_message.clone(),
2518                    blocking: false,
2519                }),
2520            ) {
2521                warn!("failed to write chat stream failure profile event: {}", err);
2522            }
2523            let engine_status = if chat_resource_failure_kind(failure_kind) {
2524                Some(engine.status().await)
2525            } else {
2526                None
2527            };
2528            error!(
2529                "Stream generation failed before first chunk: {}",
2530                error_message
2531            );
2532            if let Err(err) = write_chat_request_failure_diagnostics(
2533                &state,
2534                &replay_request_id,
2535                failure_kind,
2536                "chat_completions_stream_start",
2537                error_kind,
2538                &error_message,
2539                engine_status.as_ref(),
2540            ) {
2541                warn!("failed to write chat stream failure diagnostics: {}", err);
2542            }
2543            return Err(server_error_from_ferrum_error(e));
2544        }
2545    };
2546    let request_dump_dir = state.request_dump_dir.clone();
2547    let admission_summary = state
2548        .auto_config
2549        .as_ref()
2550        .map(|config| config.admission_summary_document());
2551    let diagnostics_engine = engine.clone();
2552    let profile_state = state.clone();
2553
2554    tokio::spawn(async move {
2555        let mut current_text = String::new();
2556        let mut output_token_ids = Vec::new();
2557        let mut first_engine_chunk_received_us = None;
2558        let mut first_sse_enqueue_us = None;
2559        let mut sent_reasoning_len = 0usize;
2560        let mut sent_content_len = 0usize;
2561
2562        loop {
2563            let next = tokio::select! {
2564                biased;
2565                _ = tx.closed() => break,
2566                next = stream.next() => next,
2567            };
2568            let Some(result) = next else {
2569                break;
2570            };
2571            match result {
2572                Ok(chunk) => {
2573                    if first_engine_chunk_received_us.is_none()
2574                        && (chunk.token.is_some() || !chunk.text.is_empty())
2575                    {
2576                        first_engine_chunk_received_us = Some(elapsed_us_since(profile_started_at));
2577                    }
2578                    if let Some(token) = chunk.token {
2579                        output_token_ids.push(token);
2580                    }
2581                    if !chunk.text.is_empty() {
2582                        current_text.push_str(&chunk.text);
2583                        if let Some(projector) = native_projector.as_mut() {
2584                            projector.push(&chunk.text);
2585                        }
2586
2587                        if native_projector.is_some()
2588                            || (!buffer_stream_output
2589                                && !should_defer_model_reasoning_stream_delta(
2590                                    model_output_protocol,
2591                                    &current_text,
2592                                ))
2593                        {
2594                            let parsed_result = if let Some(projector) = native_projector.as_ref() {
2595                                Ok(ParsedReasoningResponse {
2596                                    content: projector.visible_prefix().to_owned(),
2597                                    reasoning: projector.reasoning_prefix().map(str::to_owned),
2598                                })
2599                            } else {
2600                                parse_model_reasoning_response(
2601                                    model_output_protocol,
2602                                    &current_text,
2603                                    started_in_think,
2604                                )
2605                            };
2606                            let parsed = match parsed_result {
2607                                Ok(parsed) => parsed,
2608                                Err(error) => {
2609                                    let _ = tx.send(Ok(openai_error_sse_event(
2610                                        error.to_string(),
2611                                        "internal_server_error",
2612                                        Some("model_output"),
2613                                    )));
2614                                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
2615                                    break;
2616                                }
2617                            };
2618                            let full_reasoning = parsed.reasoning.as_deref().unwrap_or("");
2619                            let reasoning_delta =
2620                                stream_text_delta(full_reasoning, &mut sent_reasoning_len);
2621                            let content_delta =
2622                                stream_text_delta(&parsed.content, &mut sent_content_len);
2623                            if !reasoning_delta.is_empty() || !content_delta.is_empty() {
2624                                // Create streaming response chunk
2625                                let response_chunk = ChatCompletionsResponse {
2626                                    id: request_id.clone(),
2627                                    object: "chat.completion.chunk".to_string(),
2628                                    created: chrono::Utc::now().timestamp() as u64,
2629                                    model: openai_request.model.clone(),
2630                                    choices: vec![ChatChoice {
2631                                        index: 0,
2632                                        message: None,
2633                                        delta: Some(ChatMessage {
2634                                            role: MessageRole::Assistant,
2635                                            content: content_delta,
2636                                            reasoning: (!reasoning_delta.is_empty())
2637                                                .then_some(reasoning_delta),
2638                                            name: None,
2639                                            tool_calls: None,
2640                                            tool_call_id: None,
2641                                            function_call: None,
2642                                        }),
2643                                        finish_reason: None,
2644                                    }],
2645                                    usage: None,
2646                                };
2647
2648                                let sse_event = Event::default()
2649                                    .json_data(&response_chunk)
2650                                    .unwrap_or_else(|_| Event::default().data("error"));
2651                                if tx.send(Ok(sse_event)).is_err() {
2652                                    break;
2653                                }
2654                                first_sse_enqueue_us
2655                                    .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2656                            }
2657                        }
2658                    }
2659
2660                    if chunk.finish_reason.is_some() {
2661                        let terminal_finish_reason = chunk
2662                            .finish_reason
2663                            .expect("finish_reason presence checked above");
2664                        if let Err(err) = write_chat_prompt_token_evidence(
2665                            request_dump_dir.as_ref().map(|root| root.as_path()),
2666                            &replay_request_id,
2667                            &profile_request_model,
2668                            chunk.execution_evidence.as_ref(),
2669                        ) {
2670                            warn!("failed to write chat stream prompt-token evidence: {}", err);
2671                        }
2672                        let usage = chunk.usage.as_ref().map(openai_usage_from_token_usage);
2673                        let native_projected = native_projector
2674                            .take()
2675                            .map(|projector| projector.finish(terminal_finish_reason));
2676                        let parsed_output_result =
2677                            if let Some(projected) = native_projected.as_ref() {
2678                                Ok(ParsedChatModelOutput {
2679                                    visible: projected.visible.clone(),
2680                                    harmony_response: None,
2681                                })
2682                            } else {
2683                                parse_chat_model_output(
2684                                    model_output_protocol,
2685                                    &current_text,
2686                                    started_in_think,
2687                                    terminal_finish_reason,
2688                                )
2689                            };
2690                        let parsed_model_output = match parsed_output_result {
2691                            Ok(parsed) => parsed,
2692                            Err(error) => {
2693                                let error_event = openai_error_sse_event(
2694                                    stream_validation_error_message(error),
2695                                    "internal_server_error",
2696                                    Some("model_output"),
2697                                );
2698                                let _ = tx.send(Ok(error_event));
2699                                let _ = tx.send(Ok(Event::default().data("[DONE]")));
2700                                break;
2701                            }
2702                        };
2703                        let mut parsed_final = parsed_model_output.visible;
2704                        parsed_final.content = normalize_structured_response_content(
2705                            &openai_request,
2706                            &parsed_final.content,
2707                        );
2708                        let mut structured_chat_response =
2709                            finish_reason_allows_structured_api_response(terminal_finish_reason)
2710                                .then(|| match chunk.api_response.as_ref() {
2711                                    // The native envelope owns the recipient and whether this is
2712                                    // a call at all, including a final answer containing JSON.
2713                                    _ if model_output_protocol
2714                                        == ModelOutputProtocol::HarmonyGptOss =>
2715                                    {
2716                                        parsed_model_output.harmony_response.clone()
2717                                    }
2718                                    Some(ferrum_types::ApiResponse::Chat(response)) => {
2719                                        Some(response.clone())
2720                                    }
2721                                    _ if native_projected.is_some() => native_projected
2722                                        .as_ref()
2723                                        .and_then(|projected| projected.api_response.clone()),
2724                                    _ if buffer_structured_api_stream => {
2725                                        chat_api_response_from_parsed_generated_text(
2726                                            &stream_api_request,
2727                                            &parsed_final,
2728                                            terminal_finish_reason,
2729                                        )
2730                                    }
2731                                    _ => None,
2732                                })
2733                                .flatten();
2734
2735                        if native_projected.is_none()
2736                            && matches!(
2737                                chunk.api_response,
2738                                Some(ferrum_types::ApiResponse::Chat(_))
2739                            )
2740                        {
2741                            if let Some(response) = structured_chat_response.as_mut() {
2742                                if let Err(error) = project_typed_tool_response_content(
2743                                    response,
2744                                    model_output_protocol,
2745                                    started_in_think,
2746                                ) {
2747                                    let _ = tx.send(Ok(openai_error_sse_event(
2748                                        stream_validation_error_message(error),
2749                                        "internal_server_error",
2750                                        Some("model_output"),
2751                                    )));
2752                                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
2753                                    break;
2754                                }
2755                            }
2756                        }
2757
2758                        if let Some(chat_response) = structured_chat_response.as_ref() {
2759                            if let Err(e) =
2760                                validate_structured_tool_response(&openai_request, chat_response)
2761                            {
2762                                let error_event = openai_error_sse_event(
2763                                    stream_validation_error_message(e),
2764                                    "internal_server_error",
2765                                    Some("tool_choice"),
2766                                );
2767                                let _ = tx.send(Ok(error_event));
2768                                let _ = tx.send(Ok(Event::default().data("[DONE]")));
2769                                break;
2770                            }
2771                        } else if tool_choice_required(&openai_request) {
2772                            log_required_tool_choice_failure(
2773                                &openai_request,
2774                                &parsed_final.content,
2775                                parsed_final.reasoning.as_deref(),
2776                            );
2777                            let error_event = openai_error_sse_event(
2778                                "model output did not satisfy required tool_choice",
2779                                "invalid_request_error",
2780                                Some("tool_choice"),
2781                            );
2782                            let _ = tx.send(Ok(error_event));
2783                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2784                            break;
2785                        }
2786                        if let Err(e) = validate_hard_structured_response(
2787                            &openai_request,
2788                            &parsed_final.content,
2789                            structured_chat_response.as_ref(),
2790                        ) {
2791                            let error_event = openai_error_sse_event(
2792                                stream_validation_error_message(e),
2793                                "internal_server_error",
2794                                structured_response_error_param(output_contract),
2795                            );
2796                            let _ = tx.send(Ok(error_event));
2797                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2798                            break;
2799                        }
2800
2801                        if let Some(chat_response) = structured_chat_response.as_ref() {
2802                            let mut delta = openai_chat_delta_from_api(&chat_response.message);
2803                            if native_projected.is_some() {
2804                                delta.content =
2805                                    stream_text_delta(&delta.content, &mut sent_content_len);
2806                            }
2807                            if delta.reasoning.is_none() {
2808                                delta.reasoning = parsed_final.reasoning.clone();
2809                            }
2810                            if native_projected.is_some() {
2811                                let reasoning = stream_text_delta(
2812                                    delta.reasoning.as_deref().unwrap_or(""),
2813                                    &mut sent_reasoning_len,
2814                                );
2815                                delta.reasoning = (!reasoning.is_empty()).then_some(reasoning);
2816                            }
2817                            let response_chunk = ChatCompletionsResponse {
2818                                id: request_id.clone(),
2819                                object: "chat.completion.chunk".to_string(),
2820                                created: chrono::Utc::now().timestamp() as u64,
2821                                model: openai_request.model.clone(),
2822                                choices: vec![ChatChoice {
2823                                    index: 0,
2824                                    message: None,
2825                                    delta: Some(delta),
2826                                    finish_reason: None,
2827                                }],
2828                                usage: None,
2829                            };
2830
2831                            let sse_event = Event::default()
2832                                .json_data(&response_chunk)
2833                                .unwrap_or_else(|_| Event::default().data("error"));
2834                            if tx.send(Ok(sse_event)).is_err() {
2835                                break;
2836                            }
2837                            first_sse_enqueue_us
2838                                .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2839                        } else if buffer_structured_api_stream
2840                            && parsed_final.content.trim().is_empty()
2841                            // A token-limited response may contain only reasoning.
2842                            // Preserve its terminal reason and usage, as sync does;
2843                            // required tool and hard content contracts were checked above.
2844                            && terminal_finish_reason != FinishReason::Length
2845                        {
2846                            let error_event = openai_error_sse_event(
2847                                "model output did not satisfy tool/function call request",
2848                                "internal_server_error",
2849                                Some("tool_choice"),
2850                            );
2851                            let _ = tx.send(Ok(error_event));
2852                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2853                            break;
2854                        } else if !current_text.is_empty() {
2855                            // Flush safe text held with an incomplete trailing
2856                            // marker, including when text and finish share a chunk.
2857                            // Buffered streams have sent lengths of zero.
2858                            let content_delta =
2859                                stream_text_delta(&parsed_final.content, &mut sent_content_len);
2860                            let reasoning_delta = stream_text_delta(
2861                                parsed_final.reasoning.as_deref().unwrap_or(""),
2862                                &mut sent_reasoning_len,
2863                            );
2864                            if !content_delta.is_empty() || !reasoning_delta.is_empty() {
2865                                let response_chunk = ChatCompletionsResponse {
2866                                    id: request_id.clone(),
2867                                    object: "chat.completion.chunk".to_string(),
2868                                    created: chrono::Utc::now().timestamp() as u64,
2869                                    model: openai_request.model.clone(),
2870                                    choices: vec![ChatChoice {
2871                                        index: 0,
2872                                        message: None,
2873                                        delta: Some(ChatMessage {
2874                                            role: MessageRole::Assistant,
2875                                            content: content_delta,
2876                                            reasoning: (!reasoning_delta.is_empty())
2877                                                .then_some(reasoning_delta),
2878                                            name: None,
2879                                            tool_calls: None,
2880                                            tool_call_id: None,
2881                                            function_call: None,
2882                                        }),
2883                                        finish_reason: None,
2884                                    }],
2885                                    usage: None,
2886                                };
2887
2888                                let sse_event = Event::default()
2889                                    .json_data(&response_chunk)
2890                                    .unwrap_or_else(|_| Event::default().data("error"));
2891                                if tx.send(Ok(sse_event)).is_err() {
2892                                    break;
2893                                }
2894                                first_sse_enqueue_us
2895                                    .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2896                            }
2897                        }
2898                        // Send final chunk. OpenAI-style streaming
2899                        // clients (e.g. `vllm bench serve`) blindly
2900                        // read `choices[0]["delta"]` on every chunk
2901                        // that has any `choices` entries, so the
2902                        // last chunk must include `delta` even when
2903                        // empty. Skipping it triggers
2904                        // `KeyError: 'delta'` on the client side
2905                        // and the request is reported as failed
2906                        // despite returning a 200 with content.
2907                        let final_finish_reason = structured_chat_response
2908                            .as_ref()
2909                            .and_then(|response| response.finish_reason.clone())
2910                            .or_else(|| chunk.finish_reason.as_ref().map(finish_reason_to_string))
2911                            .or(Some("length".to_string()));
2912                        let final_chunk = ChatCompletionsResponse {
2913                            id: request_id.clone(),
2914                            object: "chat.completion.chunk".to_string(),
2915                            created: chrono::Utc::now().timestamp() as u64,
2916                            model: openai_request.model.clone(),
2917                            choices: vec![ChatChoice {
2918                                index: 0,
2919                                message: None,
2920                                delta: Some(ChatMessage {
2921                                    role: MessageRole::Assistant,
2922                                    content: String::new(),
2923                                    reasoning: None,
2924                                    name: None,
2925                                    tool_calls: None,
2926                                    tool_call_id: None,
2927                                    function_call: None,
2928                                }),
2929                                finish_reason: final_finish_reason.clone(),
2930                            }],
2931                            usage: None,
2932                        };
2933
2934                        let final_event = Event::default()
2935                            .json_data(&final_chunk)
2936                            .unwrap_or_else(|_| Event::default().data("error"));
2937                        if tx.send(Ok(final_event)).is_ok() {
2938                            first_sse_enqueue_us
2939                                .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2940                        }
2941                        let completion_token_count = chunk
2942                            .usage
2943                            .as_ref()
2944                            .map(|usage| usage.completion_tokens)
2945                            .unwrap_or(output_token_ids.len());
2946                        let replay_output_token_ids = chunk
2947                            .execution_evidence
2948                            .as_ref()
2949                            .map(|evidence| evidence.output_token_ids.as_slice())
2950                            .filter(|tokens| tokens.len() == completion_token_count)
2951                            .unwrap_or(output_token_ids.as_slice());
2952                        if let Err(err) = write_chat_request_completion_replay_bundle(
2953                            request_dump_dir.as_ref().map(|root| root.as_path()),
2954                            &replay_request_id,
2955                            &parsed_final.content,
2956                            replay_output_token_ids,
2957                            final_finish_reason.as_deref(),
2958                        ) {
2959                            warn!("failed to write chat stream replay bundle: {}", err);
2960                        }
2961                        if let Err(err) = write_chat_request_profile_event(
2962                            &profile_state,
2963                            &replay_request_id,
2964                            benchmark_correlation.as_ref(),
2965                            &profile_request_model,
2966                            true,
2967                            "chat_completions_stream_complete",
2968                            profile_started_at,
2969                            ChatRequestProfileTiming {
2970                                engine_evidence: chunk.execution_evidence.as_ref(),
2971                                first_engine_chunk_received_us,
2972                                first_sse_enqueue_us,
2973                            },
2974                            completion_token_count,
2975                            chunk.usage.as_ref(),
2976                            final_finish_reason.as_deref(),
2977                            None,
2978                        ) {
2979                            warn!("failed to write chat stream profile event: {}", err);
2980                        }
2981                        if let Err(err) = maybe_write_first_request_memory_stage(
2982                            &profile_state,
2983                            &replay_request_id,
2984                            benchmark_correlation.as_ref(),
2985                            &profile_request_model,
2986                            true,
2987                            profile_started_at,
2988                            request_memory_before,
2989                        ) {
2990                            warn!("failed to write chat stream memory profile event: {}", err);
2991                        }
2992                        if include_stream_usage && usage.is_some() {
2993                            let usage_chunk = ChatCompletionsResponse {
2994                                id: request_id.clone(),
2995                                object: "chat.completion.chunk".to_string(),
2996                                created: chrono::Utc::now().timestamp() as u64,
2997                                model: openai_request.model.clone(),
2998                                choices: vec![],
2999                                usage,
3000                            };
3001                            let usage_event = Event::default()
3002                                .json_data(&usage_chunk)
3003                                .unwrap_or_else(|_| Event::default().data("error"));
3004                            let _ = tx.send(Ok(usage_event));
3005                        }
3006                        let _ = tx.send(Ok(Event::default().data("[DONE]")));
3007                        break;
3008                    }
3009                }
3010                Err(e) => {
3011                    let failure_kind = e.observability_failure_kind();
3012                    let error_kind = e.observability_error_kind();
3013                    let error_message = e.to_string();
3014                    let engine_status = if chat_resource_failure_kind(failure_kind) {
3015                        Some(diagnostics_engine.status().await)
3016                    } else {
3017                        None
3018                    };
3019                    error!("Stream generation error: {}", error_message);
3020                    if let Err(err) = write_chat_request_profile_event(
3021                        &profile_state,
3022                        &replay_request_id,
3023                        benchmark_correlation.as_ref(),
3024                        &profile_request_model,
3025                        true,
3026                        "chat_completions_stream_next",
3027                        profile_started_at,
3028                        ChatRequestProfileTiming {
3029                            engine_evidence: None,
3030                            first_engine_chunk_received_us,
3031                            first_sse_enqueue_us,
3032                        },
3033                        output_token_ids.len(),
3034                        None,
3035                        Some("error"),
3036                        Some(ProfileError {
3037                            kind: error_kind.to_string(),
3038                            message: error_message.clone(),
3039                            blocking: false,
3040                        }),
3041                    ) {
3042                        warn!("failed to write chat stream chunk profile event: {}", err);
3043                    }
3044                    if let Err(err) = write_chat_request_failure_diagnostics_at_root(
3045                        request_dump_dir.as_ref().map(|root| root.as_path()),
3046                        admission_summary.as_ref(),
3047                        engine_status.as_ref(),
3048                        &replay_request_id,
3049                        failure_kind,
3050                        "chat_completions_stream_next",
3051                        error_kind,
3052                        &error_message,
3053                    ) {
3054                        warn!(
3055                            "failed to write chat stream chunk failure diagnostics: {}",
3056                            err
3057                        );
3058                    }
3059                    let _ = tx.send(Ok(openai_error_sse_event(
3060                        error_message,
3061                        "internal_server_error",
3062                        None,
3063                    )));
3064                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
3065                    break;
3066                }
3067            }
3068        }
3069    });
3070
3071    let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
3072    let sse_stream = Sse::new(stream);
3073
3074    Ok(sse_stream.into_response())
3075}
3076
3077/// Handle non-streaming chat completions
3078async fn handle_chat_completions_sync(
3079    state: AppState,
3080    openai_request: ChatCompletionsRequest,
3081    inference_request: InferenceRequest,
3082    session_context: Option<SessionContext>,
3083    benchmark_correlation: Option<BenchmarkRequestCorrelation>,
3084) -> std::result::Result<Response, ServerError> {
3085    info!("Processing non-streaming chat completion");
3086
3087    let engine = state.llm.clone().ok_or_else(|| {
3088        ServerError::ServiceUnavailable("LLM engine not loaded; chat unavailable".into())
3089    })?;
3090    let request_chat_api = inference_request
3091        .api_request
3092        .as_ref()
3093        .and_then(|api_request| match api_request {
3094            ferrum_types::ApiRequest::Chat(chat_request) => {
3095                ferrum_types::chat_api_may_emit_tool_or_function_call(chat_request)
3096                    .then(|| chat_request.clone())
3097            }
3098            _ => None,
3099        });
3100    let model_output_protocol = inference_request.sampling_params.model_output_protocol;
3101    // R1-distill-style templates open the think block inside the prompt.
3102    let started_in_think = request_started_in_reasoning(&inference_request);
3103    let mut native_projector = NativeChatOutputProjector::for_request(&inference_request);
3104    let replay_request_id = inference_request.id.to_string();
3105    let profile_request_model = openai_request.model.clone();
3106    let profile_started_at = Instant::now();
3107    let request_memory_before = request_memory_sample_before(&state);
3108    match engine.infer(inference_request).await {
3109        Ok(output) => {
3110            let InferenceResponse {
3111                text: output_text,
3112                tokens,
3113                finish_reason,
3114                usage,
3115                api_response,
3116                execution_evidence,
3117                ..
3118            } = output;
3119            if let Err(err) = write_chat_prompt_token_evidence(
3120                state.request_dump_dir.as_ref().map(|root| root.as_path()),
3121                &replay_request_id,
3122                &profile_request_model,
3123                execution_evidence.as_ref(),
3124            ) {
3125                warn!("failed to write chat prompt-token evidence: {}", err);
3126            }
3127
3128            // OpenAI stop strings mark a boundary and are not included in the
3129            // returned completion. Hard structured formats are never repaired
3130            // here; malformed output must fail the response contract below.
3131            let stop_sequences = openai_request.stop.clone().unwrap_or_default();
3132            let content = strip_after_stop(&output_text, &stop_sequences);
3133            let native_projected = native_projector.take().map(|mut projector| {
3134                projector.push(&content);
3135                projector.finish(finish_reason)
3136            });
3137            let parsed_model_output = if let Some(projected) = native_projected.as_ref() {
3138                ParsedChatModelOutput {
3139                    visible: projected.visible.clone(),
3140                    harmony_response: None,
3141                }
3142            } else {
3143                parse_chat_model_output(
3144                    model_output_protocol,
3145                    &content,
3146                    started_in_think,
3147                    finish_reason,
3148                )?
3149            };
3150            let parsed = parsed_model_output.visible;
3151            let visible_content =
3152                normalize_structured_response_content(&openai_request, &parsed.content);
3153            let mut message = ChatMessage {
3154                role: MessageRole::Assistant,
3155                content: visible_content,
3156                reasoning: parsed.reasoning.clone(),
3157                name: None,
3158                tool_calls: None,
3159                tool_call_id: None,
3160                function_call: None,
3161            };
3162            let mut openai_finish_reason = finish_reason_to_string(&finish_reason);
3163            let mut structured_chat_response =
3164                finish_reason_allows_structured_api_response(finish_reason)
3165                    .then(|| match api_response.as_ref() {
3166                        // The native envelope owns the recipient and whether this is
3167                        // a call at all, including a final answer containing JSON.
3168                        _ if model_output_protocol == ModelOutputProtocol::HarmonyGptOss => {
3169                            parsed_model_output.harmony_response.clone()
3170                        }
3171                        Some(ferrum_types::ApiResponse::Chat(chat_response)) => {
3172                            Some(chat_response.clone())
3173                        }
3174                        _ if native_projected.is_some() => native_projected
3175                            .as_ref()
3176                            .and_then(|projected| projected.api_response.clone()),
3177                        _ => match request_chat_api.as_ref() {
3178                            Some(chat_request) => chat_api_response_from_parsed_generated_text(
3179                                chat_request,
3180                                &parsed,
3181                                finish_reason,
3182                            ),
3183                            _ => None,
3184                        },
3185                    })
3186                    .flatten();
3187            if native_projected.is_none()
3188                && matches!(api_response, Some(ferrum_types::ApiResponse::Chat(_)))
3189            {
3190                if let Some(response) = structured_chat_response.as_mut() {
3191                    project_typed_tool_response_content(
3192                        response,
3193                        model_output_protocol,
3194                        started_in_think,
3195                    )?;
3196                }
3197            }
3198            if let Some(chat_response) = structured_chat_response.as_ref() {
3199                if let Err(error) =
3200                    validate_structured_tool_response(&openai_request, chat_response)
3201                {
3202                    if let Err(err) = write_chat_request_profile_event(
3203                        &state,
3204                        &replay_request_id,
3205                        benchmark_correlation.as_ref(),
3206                        &profile_request_model,
3207                        false,
3208                        "chat_completions_sync_tool_contract",
3209                        profile_started_at,
3210                        ChatRequestProfileTiming {
3211                            engine_evidence: execution_evidence.as_ref(),
3212                            ..Default::default()
3213                        },
3214                        tokens.len(),
3215                        Some(&usage),
3216                        Some("error"),
3217                        Some(ProfileError {
3218                            kind: "tool_contract_failure".to_string(),
3219                            message: format!("{error:?}"),
3220                            blocking: true,
3221                        }),
3222                    ) {
3223                        warn!("failed to write chat tool-contract profile event: {}", err);
3224                    }
3225                    return Err(error);
3226                }
3227                message = openai_chat_message_from_api(&chat_response.message);
3228                if message.reasoning.is_none() {
3229                    message.reasoning = parsed.reasoning.clone();
3230                }
3231                if let Some(reason) = &chat_response.finish_reason {
3232                    openai_finish_reason = reason.clone();
3233                }
3234            } else if tool_choice_required(&openai_request) {
3235                log_required_tool_choice_failure(
3236                    &openai_request,
3237                    &parsed.content,
3238                    parsed.reasoning.as_deref(),
3239                );
3240                if let Err(err) = write_chat_request_profile_event(
3241                    &state,
3242                    &replay_request_id,
3243                    benchmark_correlation.as_ref(),
3244                    &profile_request_model,
3245                    false,
3246                    "chat_completions_sync_tool_choice",
3247                    profile_started_at,
3248                    ChatRequestProfileTiming {
3249                        engine_evidence: execution_evidence.as_ref(),
3250                        ..Default::default()
3251                    },
3252                    tokens.len(),
3253                    Some(&usage),
3254                    Some("error"),
3255                    Some(ProfileError {
3256                        kind: "required_tool_failure".to_string(),
3257                        message: "model output did not satisfy required tool_choice".to_string(),
3258                        blocking: true,
3259                    }),
3260                ) {
3261                    warn!("failed to write chat tool-choice profile event: {}", err);
3262                }
3263                return Err(ServerError::invalid_request(
3264                    "model output did not satisfy required tool_choice",
3265                    Some("tool_choice"),
3266                ));
3267            }
3268            if let Err(error) = validate_hard_structured_response(
3269                &openai_request,
3270                &message.content,
3271                structured_chat_response.as_ref(),
3272            ) {
3273                if let Err(err) = write_chat_request_profile_event(
3274                    &state,
3275                    &replay_request_id,
3276                    benchmark_correlation.as_ref(),
3277                    &profile_request_model,
3278                    false,
3279                    "chat_completions_sync_structured_output",
3280                    profile_started_at,
3281                    ChatRequestProfileTiming {
3282                        engine_evidence: execution_evidence.as_ref(),
3283                        ..Default::default()
3284                    },
3285                    tokens.len(),
3286                    Some(&usage),
3287                    Some("error"),
3288                    Some(ProfileError {
3289                        kind: "structured_output_failure".to_string(),
3290                        message: format!("{error:?}"),
3291                        blocking: true,
3292                    }),
3293                ) {
3294                    warn!("failed to write chat strict-schema profile event: {}", err);
3295                }
3296                return Err(error);
3297            }
3298            if let Err(err) = write_chat_request_completion_replay_bundle(
3299                state.request_dump_dir.as_ref().map(|root| root.as_path()),
3300                &replay_request_id,
3301                &message.content,
3302                &tokens,
3303                Some(&openai_finish_reason),
3304            ) {
3305                warn!("failed to write chat completion replay bundle: {}", err);
3306            }
3307            if let Err(err) = write_chat_request_profile_event(
3308                &state,
3309                &replay_request_id,
3310                benchmark_correlation.as_ref(),
3311                &profile_request_model,
3312                false,
3313                "chat_completions_sync_complete",
3314                profile_started_at,
3315                ChatRequestProfileTiming {
3316                    engine_evidence: execution_evidence.as_ref(),
3317                    ..Default::default()
3318                },
3319                tokens.len(),
3320                Some(&usage),
3321                Some(&openai_finish_reason),
3322                None,
3323            ) {
3324                warn!("failed to write chat sync profile event: {}", err);
3325            }
3326            if let Err(err) = maybe_write_first_request_memory_stage(
3327                &state,
3328                &replay_request_id,
3329                benchmark_correlation.as_ref(),
3330                &profile_request_model,
3331                false,
3332                profile_started_at,
3333                request_memory_before,
3334            ) {
3335                warn!("failed to write chat sync memory profile event: {}", err);
3336            }
3337            state
3338                .cache
3339                .update_session(session_context, message.clone(), &CachePolicy::current());
3340            let response = ChatCompletionsResponse {
3341                id: replay_request_id,
3342                object: "chat.completion".to_string(),
3343                created: chrono::Utc::now().timestamp() as u64,
3344                model: openai_request.model,
3345                choices: vec![ChatChoice {
3346                    index: 0,
3347                    message: Some(message),
3348                    delta: None,
3349                    finish_reason: Some(openai_finish_reason),
3350                }],
3351                usage: Some(openai_usage_from_token_usage(&usage)),
3352            };
3353
3354            Ok(Json(response).into_response())
3355        }
3356        Err(e) => {
3357            let failure_kind = e.observability_failure_kind();
3358            let error_kind = e.observability_error_kind();
3359            let error_message = e.to_string();
3360            let engine_status = if chat_resource_failure_kind(failure_kind) {
3361                Some(engine.status().await)
3362            } else {
3363                None
3364            };
3365            error!("Generation failed: {}", error_message);
3366            if let Err(err) = write_chat_request_profile_event(
3367                &state,
3368                &replay_request_id,
3369                benchmark_correlation.as_ref(),
3370                &profile_request_model,
3371                false,
3372                "chat_completions_sync",
3373                profile_started_at,
3374                ChatRequestProfileTiming::default(),
3375                0,
3376                None,
3377                Some("error"),
3378                Some(ProfileError {
3379                    kind: error_kind.to_string(),
3380                    message: error_message.clone(),
3381                    blocking: false,
3382                }),
3383            ) {
3384                warn!("failed to write chat sync failure profile event: {}", err);
3385            }
3386            if let Err(err) = write_chat_request_failure_diagnostics(
3387                &state,
3388                &replay_request_id,
3389                failure_kind,
3390                "chat_completions_sync",
3391                error_kind,
3392                &error_message,
3393                engine_status.as_ref(),
3394            ) {
3395                warn!(
3396                    "failed to write chat generation failure diagnostics: {}",
3397                    err
3398                );
3399            }
3400            Err(server_error_from_ferrum_error(e))
3401        }
3402    }
3403}
3404
3405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3406enum EffectiveChatOutputContract {
3407    RequiredToolCall,
3408    StrictJsonSchemaContent,
3409    JsonObjectContent,
3410    BestEffortJsonSchemaContent,
3411    Text,
3412}
3413
3414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3415enum ChatOutputBudget {
3416    AutoCeiling(u32),
3417    Explicit(u32),
3418}
3419
3420impl ChatOutputBudget {
3421    fn resolve(request: &ChatCompletionsRequest) -> Self {
3422        request
3423            .max_completion_tokens
3424            .or(request.max_tokens)
3425            .map(Self::Explicit)
3426            .unwrap_or(Self::AutoCeiling(DEFAULT_COMPLETION_MAX_TOKENS))
3427    }
3428
3429    const fn ceiling(self) -> u32 {
3430        match self {
3431            Self::AutoCeiling(value) | Self::Explicit(value) => value,
3432        }
3433    }
3434
3435    const fn is_auto(self) -> bool {
3436        matches!(self, Self::AutoCeiling(_))
3437    }
3438}
3439
3440impl EffectiveChatOutputContract {
3441    fn resolve(request: &ChatCompletionsRequest) -> Self {
3442        if tool_choice_required(request) {
3443            return Self::RequiredToolCall;
3444        }
3445        let Some(format) = request.response_format.as_ref() else {
3446            return Self::Text;
3447        };
3448        match format.format_type.as_str() {
3449            "json_schema"
3450                if format
3451                    .json_schema
3452                    .as_ref()
3453                    .and_then(|schema| schema.strict)
3454                    .unwrap_or(false) =>
3455            {
3456                Self::StrictJsonSchemaContent
3457            }
3458            "json_schema" => Self::BestEffortJsonSchemaContent,
3459            "json_object" => Self::JsonObjectContent,
3460            _ => Self::Text,
3461        }
3462    }
3463
3464    fn accepts_requested_response_format(self) -> bool {
3465        !matches!(self, Self::RequiredToolCall)
3466    }
3467}
3468
3469/// Convert OpenAI chat request to internal inference request
3470#[allow(dead_code)]
3471fn convert_chat_request(
3472    request: &ChatCompletionsRequest,
3473) -> ferrum_types::Result<InferenceRequest> {
3474    convert_chat_request_with_template_model(request, &request.model, None)
3475}
3476
3477fn request_started_in_reasoning(request: &InferenceRequest) -> bool {
3478    request
3479        .metadata
3480        .get(PROMPT_OPENED_REASONING_METADATA_KEY)
3481        .and_then(serde_json::Value::as_bool)
3482        .unwrap_or_else(|| {
3483            // Requests constructed outside the chat converter retain their
3484            // legacy parser behavior. Rendered chat requests carry the actual
3485            // generation-suffix state, independent of user/schema text.
3486            has_unclosed_model_reasoning_block(
3487                request.sampling_params.model_output_protocol,
3488                &request.prompt,
3489            )
3490        })
3491}
3492
3493/// Convert OpenAI chat request to internal inference request.
3494///
3495/// `template_model_id` is the loaded model id used for prompt-template family
3496/// detection. The request `model` field may be an OpenAI-compatible alias such
3497/// as "ferrum"; using it for template selection can feed a fallback prompt to
3498/// a Qwen/Llama model.
3499fn convert_chat_request_with_template_model(
3500    request: &ChatCompletionsRequest,
3501    template_model_id: &str,
3502    model_template: Option<&ModelChatTemplate>,
3503) -> ferrum_types::Result<InferenceRequest> {
3504    convert_chat_request_with_template_model_and_default(
3505        request,
3506        template_model_id,
3507        model_template,
3508        None,
3509        true,
3510        None,
3511    )
3512}
3513
3514fn convert_chat_request_with_template_model_and_default(
3515    request: &ChatCompletionsRequest,
3516    template_model_id: &str,
3517    model_template: Option<&ModelChatTemplate>,
3518    default_enable_thinking: Option<bool>,
3519    interleaved_system_coalescing: bool,
3520    message_phases: Option<&[Option<AssistantMessagePhase>]>,
3521) -> ferrum_types::Result<InferenceRequest> {
3522    let no_tools: &[ChatTool] = &[];
3523    let tools = if tool_choice_none_hides_tools(request.tool_choice.as_ref(), model_template) {
3524        no_tools
3525    } else {
3526        request.tools.as_deref().unwrap_or_default()
3527    };
3528    let default_tool_choice =
3529        default_auto_tool_choice_for_tools(tools, request.tool_choice.as_ref());
3530    let effective_tool_choice = request
3531        .tool_choice
3532        .as_ref()
3533        .or(default_tool_choice.as_ref());
3534    let functions = request.functions.as_deref().unwrap_or_default();
3535    let model_output_protocol = model_template
3536        .map(|template| template.output_protocol)
3537        .unwrap_or(ModelOutputProtocol::Text);
3538    let output_contract = EffectiveChatOutputContract::resolve(request);
3539    let output_budget = ChatOutputBudget::resolve(request);
3540    let tool_call_protocol = model_template
3541        .map(|template| {
3542            // Harmony owns its channel/recipient envelope independently of the
3543            // generic native JSON tool grammar.
3544            if model_output_protocol == ModelOutputProtocol::HarmonyGptOss
3545                && template.tool_call_protocol == ferrum_types::ApiToolCallProtocol::NativeJson
3546            {
3547                ferrum_types::ApiToolCallProtocol::Json
3548            } else {
3549                template.tool_call_protocol
3550            }
3551        })
3552        .unwrap_or_default();
3553    let api_chat = api_chat_request(request, effective_tool_choice, tool_call_protocol);
3554    let native_tool_call_contract = api_chat.requires_native_tool_call();
3555    // Harmony tool calls own their complete channel/message/call envelope.
3556    // Applying the generic tool-argument JSON grammar at token zero would
3557    // mask that envelope and force the model to emit bare arguments instead.
3558    let forced_response_format = (model_output_protocol != ModelOutputProtocol::HarmonyGptOss
3559        && !native_tool_call_contract)
3560        .then(|| forced_tool_choice_response_format(request))
3561        .flatten();
3562    let hard_tool_call_contract = forced_response_format.is_some() || native_tool_call_contract;
3563    let requested_response_format = output_contract
3564        .accepts_requested_response_format()
3565        .then(|| requested_response_format_for_sampling(request))
3566        .transpose()?
3567        .flatten();
3568    let chat_template_options =
3569        chat_template_options_for_request(request, model_template, default_enable_thinking)?;
3570    let response_format = forced_response_format
3571        .or(requested_response_format)
3572        .unwrap_or(ferrum_types::ResponseFormat::Text);
3573    let model_generated_thinking = model_template.is_some_and(|template| {
3574        template.reasoning_protocol == ModelReasoningProtocol::ModelGenerated
3575            && template.reasoning_enabled(chat_template_options.enable_thinking)
3576    });
3577    let reasoning_enabled = model_template
3578        .is_some_and(|template| template.reasoning_enabled(chat_template_options.enable_thinking));
3579    let (render_messages, render_message_phases) = render_messages_with_response_format_instruction(
3580        request,
3581        output_contract,
3582        reasoning_enabled,
3583        model_template,
3584        message_phases,
3585    );
3586    let rendered_prompt = if tools.is_empty() && functions.is_empty() {
3587        render_chat_prompt_with_model_template_options_and_compatibility_with_prefill(
3588            &render_messages,
3589            template_model_id,
3590            model_template,
3591            &chat_template_options,
3592            interleaved_system_coalescing,
3593            Some(&render_message_phases),
3594        )?
3595    } else {
3596        render_chat_prompt_with_tools_and_model_template_compatibility_with_prefill(
3597            &render_messages,
3598            template_model_id,
3599            model_template,
3600            &chat_template_options,
3601            tools,
3602            effective_tool_choice,
3603            functions,
3604            request.function_call.as_ref(),
3605            interleaved_system_coalescing,
3606            Some(&render_message_phases),
3607        )?
3608    };
3609    let prompt = rendered_prompt.text;
3610    let prompt_opened_thinking = rendered_prompt.reasoning_prefill;
3611    let mut metadata = HashMap::new();
3612    metadata.insert(
3613        PROMPT_OPENED_REASONING_METADATA_KEY.to_string(),
3614        serde_json::Value::Bool(prompt_opened_thinking),
3615    );
3616    metadata.insert(
3617        "openai_messages".to_string(),
3618        serde_json::to_value(&request.messages)?,
3619    );
3620    if let Some(tools) = &request.tools {
3621        metadata.insert("openai_tools".to_string(), serde_json::to_value(tools)?);
3622    }
3623    if let Some(tool_choice) = effective_tool_choice {
3624        metadata.insert(
3625            "openai_tool_choice".to_string(),
3626            serde_json::to_value(tool_choice)?,
3627        );
3628    }
3629    if let Some(functions) = &request.functions {
3630        metadata.insert(
3631            "openai_legacy_functions".to_string(),
3632            serde_json::to_value(functions)?,
3633        );
3634    }
3635    if let Some(function_call) = &request.function_call {
3636        metadata.insert(
3637            "openai_legacy_function_call".to_string(),
3638            serde_json::to_value(function_call)?,
3639        );
3640    }
3641    if request.ignore_eos.unwrap_or(false) {
3642        metadata.insert("ferrum_ignore_eos".to_string(), serde_json::json!(true));
3643    }
3644    if output_budget.is_auto() {
3645        metadata.insert(
3646            DEFAULT_MAX_TOKENS_METADATA_KEY.to_string(),
3647            serde_json::json!(true),
3648        );
3649    }
3650    let reasoning_markers = model_reasoning_markers(model_output_protocol);
3651    if !prompt_opened_thinking {
3652        let mut forbidden = reasoning_markers
3653            .map(|(_, close)| vec![close.to_string()])
3654            .unwrap_or_default();
3655        if hard_tool_call_contract {
3656            for token_text in INITIAL_STRUCTURED_CALL_FORBIDDEN_TOKEN_TEXTS {
3657                push_unique_forbidden_token_text(&mut forbidden, token_text);
3658            }
3659            if let Some(eos) = model_template.as_ref().and_then(|template| {
3660                template
3661                    .eos_token
3662                    .as_deref()
3663                    .filter(|token| !token.is_empty())
3664            }) {
3665                push_unique_forbidden_token_text(&mut forbidden, eos);
3666            }
3667        }
3668        if model_output_protocol == ModelOutputProtocol::Text
3669            && chat_template_options.enable_thinking == Some(false)
3670            && model_template
3671                .is_some_and(|template| template.reasoning_protocol.supports_reasoning())
3672        {
3673            push_unique_forbidden_token_text(&mut forbidden, THINK_START_TAG);
3674        }
3675        metadata.insert(
3676            INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY.to_string(),
3677            serde_json::json!(forbidden),
3678        );
3679    }
3680    let structured_output =
3681        !matches!(response_format, ferrum_types::ResponseFormat::Text) || native_tool_call_contract;
3682    let structured_output_after_reasoning = structured_output
3683        && model_output_protocol == ModelOutputProtocol::Text
3684        && (prompt_opened_thinking || model_generated_thinking);
3685    let structured_output_start =
3686        if structured_output && model_output_protocol == ModelOutputProtocol::HarmonyGptOss {
3687            StructuredOutputStart::HarmonyFinal
3688        } else if structured_output && model_output_protocol == ModelOutputProtocol::GemmaThought {
3689            let (opening, closing) = reasoning_markers.expect("Gemma thought markers");
3690            if prompt_opened_thinking {
3691                StructuredOutputStart::AfterDelimiter(closing.to_string())
3692            } else if prompt.trim_end().ends_with(closing) {
3693                StructuredOutputStart::Immediate
3694            } else {
3695                StructuredOutputStart::AfterReasoningEnvelope {
3696                    opening: opening.to_string(),
3697                    closing: closing.to_string(),
3698                    allow_reasoning: reasoning_enabled,
3699                }
3700            }
3701        } else if structured_output_after_reasoning {
3702            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
3703        } else {
3704            StructuredOutputStart::Immediate
3705        };
3706    let delayed_grammar = matches!(
3707        structured_output_start,
3708        StructuredOutputStart::AfterDelimiter(_)
3709            | StructuredOutputStart::AfterReasoningEnvelope { .. }
3710    );
3711    let response_completion_boundary = if let Some((_, closing)) =
3712        reasoning_markers.filter(|_| prompt_opened_thinking || delayed_grammar)
3713    {
3714        ResponseCompletionBoundary::AfterDelimiterAndPayload {
3715            delimiter: closing.to_string(),
3716            alternate_envelope: api_chat.generated_response_envelope(),
3717        }
3718    } else {
3719        ResponseCompletionBoundary::Immediate
3720    };
3721
3722    Ok(InferenceRequest {
3723        id: RequestId(Uuid::new_v4()),
3724        model_id: ModelId(request.model.clone()),
3725        prompt,
3726        sampling_params: SamplingParams {
3727            max_tokens: output_budget.ceiling() as usize,
3728            temperature: request.temperature.unwrap_or(DEFAULT_SAMPLING_TEMPERATURE),
3729            top_p: request.top_p.unwrap_or(DEFAULT_SAMPLING_TOP_P),
3730            top_k: request
3731                .top_k
3732                .filter(|value| *value > 0)
3733                .and_then(|value| usize::try_from(value).ok()),
3734            repetition_penalty: request
3735                .repetition_penalty
3736                .unwrap_or(DEFAULT_CHAT_REPETITION_PENALTY),
3737            presence_penalty: request.presence_penalty.unwrap_or(0.0),
3738            frequency_penalty: request.frequency_penalty.unwrap_or(0.0),
3739            stop_sequences: request.stop.clone().unwrap_or_default(),
3740            seed: request.seed,
3741            min_p: request.min_p.filter(|value| *value > 0.0),
3742            tfs: None,
3743            typical_p: None,
3744            mirostat: None,
3745            response_format,
3746            structured_output_start,
3747            response_completion_boundary,
3748            model_output_protocol,
3749        },
3750        stream: request.stream.unwrap_or(false),
3751        priority: Priority::Normal, // Default priority
3752        client_id: None,
3753        session_id: None,
3754        created_at: chrono::Utc::now(),
3755        api_request: Some(ferrum_types::ApiRequest::Chat(api_chat)),
3756        evidence_request: Default::default(),
3757        metadata,
3758    })
3759}
3760
3761fn push_unique_forbidden_token_text(tokens: &mut Vec<String>, token: &str) {
3762    if !token.is_empty() && !tokens.iter().any(|existing| existing == token) {
3763        tokens.push(token.to_string());
3764    }
3765}
3766
3767fn default_auto_tool_choice_for_tools(
3768    tools: &[ChatTool],
3769    choice: Option<&ToolChoice>,
3770) -> Option<ToolChoice> {
3771    if choice.is_none() && !tools.is_empty() {
3772        Some(ToolChoice::Mode("auto".to_string()))
3773    } else {
3774        None
3775    }
3776}
3777
3778fn tool_choice_none(choice: Option<&ToolChoice>) -> bool {
3779    matches!(choice, Some(ToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none"))
3780}
3781
3782fn tool_choice_none_hides_tools(
3783    choice: Option<&ToolChoice>,
3784    model_template: Option<&ModelChatTemplate>,
3785) -> bool {
3786    tool_choice_none(choice)
3787        && model_template
3788            .map(|template| template.template.contains("tools_in_user_message"))
3789            .unwrap_or(false)
3790}
3791
3792fn chat_template_options_for_request(
3793    request: &ChatCompletionsRequest,
3794    model_template: Option<&ModelChatTemplate>,
3795    default_enable_thinking: Option<bool>,
3796) -> ferrum_types::Result<ChatTemplateOptions> {
3797    let mut options = ChatTemplateOptions::default_for_template(model_template);
3798    let kwargs = request.chat_template_kwargs.as_ref();
3799    let explicit_thinking = kwargs
3800        .and_then(|values| values.get("enable_thinking"))
3801        .filter(|value| !value.is_null())
3802        .map(|value| {
3803            value.as_bool().ok_or_else(|| {
3804                Error::invalid_request("chat_template_kwargs.enable_thinking must be a boolean")
3805            })
3806        })
3807        .transpose()?;
3808    let extension_effort = kwargs
3809        .and_then(|values| values.get("reasoning_effort"))
3810        .filter(|value| !value.is_null())
3811        .map(|value| {
3812            serde_json::from_value::<ReasoningEffort>(value.clone()).map_err(|error| {
3813                Error::invalid_request(format!("chat_template_kwargs.reasoning_effort: {error}"))
3814            })
3815        })
3816        .transpose()?;
3817    if let (Some(standard), Some(extension)) = (request.reasoning_effort, extension_effort) {
3818        if standard != extension {
3819            return Err(Error::invalid_request(
3820                "reasoning_effort conflicts with chat_template_kwargs.reasoning_effort",
3821            ));
3822        }
3823    }
3824    options.reasoning_effort = request.reasoning_effort.or(extension_effort);
3825    let effort_thinking = request
3826        .reasoning_effort
3827        .map(|effort| effort != ReasoningEffort::None);
3828    if let (Some(explicit), Some(derived)) = (explicit_thinking, effort_thinking) {
3829        if explicit != derived {
3830            return Err(Error::invalid_request(
3831                "reasoning_effort conflicts with chat_template_kwargs.enable_thinking",
3832            ));
3833        }
3834    }
3835    // Standard effort overrides product defaults and supplies both variables.
3836    // Legacy-only kwargs keep their template-level behavior: an effort alone
3837    // must not change their thinking switch or acquire new capability checks.
3838    options.enable_thinking = explicit_thinking
3839        .or(effort_thinking)
3840        .or(default_enable_thinking);
3841    if let (Some(template), Some(effort)) = (model_template, request.reasoning_effort) {
3842        template.validate_reasoning_effort(effort)?;
3843    }
3844    Ok(options)
3845}
3846
3847fn render_messages_with_response_format_instruction(
3848    request: &ChatCompletionsRequest,
3849    output_contract: EffectiveChatOutputContract,
3850    reasoning_enabled: bool,
3851    model_template: Option<&ModelChatTemplate>,
3852    message_phases: Option<&[Option<AssistantMessagePhase>]>,
3853) -> (Vec<ChatMessage>, Vec<Option<AssistantMessagePhase>>) {
3854    let mut phases = message_phases
3855        .map(ToOwned::to_owned)
3856        .unwrap_or_else(|| vec![None; request.messages.len()]);
3857    debug_assert_eq!(phases.len(), request.messages.len());
3858    let Some(instruction) = response_format_prompt_instruction(
3859        request,
3860        output_contract,
3861        reasoning_enabled,
3862        model_template,
3863    ) else {
3864        return (request.messages.clone(), phases);
3865    };
3866    let mut messages = request.messages.clone();
3867    let leading_systems = messages
3868        .iter()
3869        .take_while(|message| message.role == MessageRole::System)
3870        .count();
3871    let mut system_parts = Vec::with_capacity(leading_systems + 1);
3872    system_parts.push(instruction);
3873    system_parts.extend(
3874        messages
3875            .drain(..leading_systems)
3876            .map(|message| message.content)
3877            .filter(|content| !content.is_empty()),
3878    );
3879    phases.drain(..leading_systems);
3880    messages.insert(
3881        0,
3882        ChatMessage {
3883            role: MessageRole::System,
3884            content: system_parts.join("\n\n"),
3885            reasoning: None,
3886            name: None,
3887            tool_calls: None,
3888            tool_call_id: None,
3889            function_call: None,
3890        },
3891    );
3892    phases.insert(0, None);
3893    (messages, phases)
3894}
3895
3896fn response_format_prompt_instruction(
3897    request: &ChatCompletionsRequest,
3898    output_contract: EffectiveChatOutputContract,
3899    reasoning_enabled: bool,
3900    model_template: Option<&ModelChatTemplate>,
3901) -> Option<String> {
3902    if !output_contract.accepts_requested_response_format() {
3903        return None;
3904    }
3905    let automatic_tools = request
3906        .tools
3907        .as_ref()
3908        .is_some_and(|tools| !tools.is_empty())
3909        && match request.tool_choice.as_ref() {
3910            None => true,
3911            Some(ToolChoice::Mode(mode)) => mode.eq_ignore_ascii_case("auto"),
3912            _ => false,
3913        }
3914        && matches!(
3915            output_contract,
3916            EffectiveChatOutputContract::StrictJsonSchemaContent
3917                | EffectiveChatOutputContract::JsonObjectContent
3918        );
3919    let native_tool_template =
3920        model_template.is_some_and(crate::chat_template::model_template_supports_tools);
3921    let tool_instruction = "If a tool is needed, put a JSON object with the declared function name in the name field and its argument object in the arguments field inside <tool_call>...</tool_call>. This envelope distinguishes a tool call from a final JSON answer.";
3922    if let Some(format) = request.response_format.as_ref() {
3923        return match format.format_type.as_str() {
3924            "json_object" if automatic_tools && native_tool_template => {
3925                Some(native_tool_final_format_instruction(r#"{"type":"object"}"#))
3926            }
3927            "json_object" if automatic_tools => Some(format!(
3928                "{tool_instruction} The response_format applies only to the final answer: output a single valid JSON object, with no markdown fences or extra text."
3929            )),
3930            "json_object" => Some(
3931                "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."
3932                    .to_string(),
3933            ),
3934            "json_schema" => {
3935                let schema = format.json_schema.as_ref()?.schema.as_ref()?;
3936                let schema_text = serde_json::to_string(schema).ok()?;
3937                Some(if automatic_tools && native_tool_template {
3938                    native_tool_final_format_instruction(&schema_text)
3939                } else if automatic_tools {
3940                    format!(
3941                        "{tool_instruction} Complete any enabled reasoning before the final answer. The response_format applies only to the final answer: output a single valid JSON value satisfying this JSON Schema, with no markdown fences or extra text. Schema: {schema_text}"
3942                    )
3943                } else if reasoning_enabled {
3944                    format!(
3945                        "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}"
3946                    )
3947                } else {
3948                    format!(
3949                        "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}"
3950                    )
3951                })
3952            }
3953            _ => None,
3954        };
3955    }
3956    None
3957}
3958
3959fn native_tool_final_format_instruction(schema: &str) -> String {
3960    // The model template owns tool selection and wire instructions. Give the
3961    // final-answer schema its own section so it is not folded into those tool
3962    // instructions, and leave the native call format unchanged.
3963    format!(
3964        "# Response Format\n\nYour final response should be a JSON value that conforms to the following schema:\n\n{schema}\n\nDo not wrap your JSON response in Markdown code blocks."
3965    )
3966}
3967
3968fn forced_tool_choice_response_format(
3969    request: &ChatCompletionsRequest,
3970) -> Option<ferrum_types::ResponseFormat> {
3971    let selected_tool = selected_tool_for_forced_tool_choice(request)?;
3972    let schema = guided_tool_arguments_schema(selected_tool.function.parameters.as_ref())?;
3973    serde_json::to_string(&schema)
3974        .ok()
3975        .map(ferrum_types::ResponseFormat::JsonSchema)
3976}
3977
3978fn requested_response_format_for_sampling(
3979    request: &ChatCompletionsRequest,
3980) -> ferrum_types::Result<Option<ferrum_types::ResponseFormat>> {
3981    let Some(format) = request.response_format.as_ref() else {
3982        return Ok(None);
3983    };
3984    match format.format_type.as_str() {
3985        "json_object" => Ok(Some(ferrum_types::ResponseFormat::JsonObject)),
3986        "json_schema" => {
3987            let Some(schema) = format.json_schema.as_ref() else {
3988                return Err(Error::invalid_request(
3989                    "response_format.json_schema.schema is required",
3990                ));
3991            };
3992            if !schema.strict.unwrap_or(false) {
3993                return Ok(None);
3994            }
3995            let Some(schema_value) = schema.schema.as_ref() else {
3996                return Err(Error::invalid_request(
3997                    "response_format.json_schema.schema is required",
3998                ));
3999            };
4000            serde_json::to_string(schema_value)
4001                .map(|schema| Some(ferrum_types::ResponseFormat::JsonSchema(schema)))
4002                .map_err(|err| Error::invalid_request(err.to_string()))
4003        }
4004        _ => Ok(None),
4005    }
4006}
4007
4008fn selected_tool_for_forced_tool_choice(request: &ChatCompletionsRequest) -> Option<&ChatTool> {
4009    match request.tool_choice.as_ref()? {
4010        ToolChoice::Function {
4011            tool_type,
4012            function,
4013        } if tool_type == "function" => request
4014            .tools
4015            .as_ref()?
4016            .iter()
4017            .find(|tool| tool.function.name == function.name),
4018        ToolChoice::Mode(mode) if mode.eq_ignore_ascii_case("required") => {
4019            single_function_tool(request.tools.as_deref()?)
4020        }
4021        _ => None,
4022    }
4023}
4024
4025fn guided_tool_arguments_schema(
4026    parameters: Option<&serde_json::Value>,
4027) -> Option<serde_json::Value> {
4028    let mut schema = parameters?.clone();
4029    bound_unconstrained_tool_argument_strings(
4030        &mut schema,
4031        DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH,
4032    );
4033    Some(schema)
4034}
4035
4036fn bound_unconstrained_tool_argument_strings(value: &mut serde_json::Value, default_max: u64) {
4037    match value {
4038        serde_json::Value::Object(map) => {
4039            let is_string = map
4040                .get("type")
4041                .and_then(serde_json::Value::as_str)
4042                .is_some_and(|ty| ty == "string");
4043            let has_finite_string_shape = map.contains_key("enum") || map.contains_key("maxLength");
4044            if is_string && !has_finite_string_shape {
4045                map.insert(
4046                    "maxLength".to_string(),
4047                    serde_json::Value::Number(default_max.into()),
4048                );
4049            }
4050            if let Some(properties) = map
4051                .get_mut("properties")
4052                .and_then(serde_json::Value::as_object_mut)
4053            {
4054                for property in properties.values_mut() {
4055                    bound_unconstrained_tool_argument_strings(property, default_max);
4056                }
4057            }
4058            if let Some(items) = map.get_mut("items") {
4059                bound_unconstrained_tool_argument_strings(items, default_max);
4060            }
4061        }
4062        serde_json::Value::Array(items) => {
4063            for item in items {
4064                bound_unconstrained_tool_argument_strings(item, default_max);
4065            }
4066        }
4067        _ => {}
4068    }
4069}
4070
4071fn single_function_tool(tools: &[ChatTool]) -> Option<&ChatTool> {
4072    let mut function_tools = tools.iter().filter(|tool| tool.tool_type == "function");
4073    let tool = function_tools.next()?;
4074    function_tools.next().is_none().then_some(tool)
4075}
4076
4077fn stream_text_delta(text: &str, sent_len: &mut usize) -> String {
4078    if *sent_len <= text.len() && text.is_char_boundary(*sent_len) {
4079        let delta = text[*sent_len..].to_string();
4080        *sent_len = text.len();
4081        return delta;
4082    }
4083    *sent_len = text.len();
4084    String::new()
4085}
4086
4087fn project_typed_tool_response_content(
4088    response: &mut ferrum_types::ApiChatResponse,
4089    protocol: ModelOutputProtocol,
4090    started_in_think: bool,
4091) -> std::result::Result<(), ServerError> {
4092    if protocol == ModelOutputProtocol::HarmonyGptOss
4093        || response.message.tool_calls.is_empty()
4094        || response.message.content.is_empty()
4095    {
4096        return Ok(());
4097    }
4098    // Engine tool parsing can precede reasoning projection. Its structural
4099    // parser already removed the envelopes and owns every call and argument;
4100    // project only the outside text, never a payload or classified final JSON.
4101    response.message.content =
4102        parse_model_reasoning_response(protocol, &response.message.content, started_in_think)
4103            .map_err(|error| ServerError::InternalError(error.to_string()))?
4104            .content;
4105    Ok(())
4106}
4107
4108fn chat_api_response_from_parsed_generated_text(
4109    chat_request: &ferrum_types::ApiChatRequest,
4110    parsed: &ParsedReasoningResponse,
4111    finish_reason: FinishReason,
4112) -> Option<ferrum_types::ApiChatResponse> {
4113    parsed
4114        .reasoning
4115        .as_deref()
4116        .and_then(|reasoning| {
4117            ferrum_types::chat_api_response_from_generated_text(
4118                chat_request,
4119                reasoning,
4120                finish_reason,
4121            )
4122        })
4123        .map(|mut response| {
4124            // Reasoning is published through its own channel. Detecting a tool
4125            // there must not promote its surrounding text to visible content.
4126            response.message.content.clear();
4127            response
4128        })
4129        .or_else(|| {
4130            ferrum_types::chat_api_response_from_generated_text(
4131                chat_request,
4132                &parsed.content,
4133                finish_reason,
4134            )
4135        })
4136}
4137
4138fn finish_reason_allows_structured_api_response(finish_reason: FinishReason) -> bool {
4139    matches!(finish_reason, FinishReason::Stop | FinishReason::EOS)
4140}
4141
4142fn log_required_tool_choice_failure(
4143    request: &ChatCompletionsRequest,
4144    content: &str,
4145    reasoning: Option<&str>,
4146) {
4147    warn!(
4148        model = %request.model,
4149        content_len = content.len(),
4150        content_head = %log_excerpt(content, 512),
4151        reasoning_len = reasoning.map(str::len).unwrap_or(0),
4152        reasoning_head = %reasoning.map(|value| log_excerpt(value, 512)).unwrap_or_default(),
4153        "model output did not satisfy required tool_choice"
4154    );
4155}
4156
4157fn log_excerpt(value: &str, max_chars: usize) -> String {
4158    let mut out = value.chars().take(max_chars).collect::<String>();
4159    if value.chars().count() > max_chars {
4160        out.push_str("...");
4161    }
4162    out
4163}
4164
4165fn normalize_structured_response_content(
4166    request: &ChatCompletionsRequest,
4167    content: &str,
4168) -> String {
4169    match EffectiveChatOutputContract::resolve(request) {
4170        EffectiveChatOutputContract::BestEffortJsonSchemaContent => {
4171            extract_json_object_text(content)
4172                .unwrap_or_else(|| strip_markdown_json_fence(content).to_string())
4173        }
4174        EffectiveChatOutputContract::RequiredToolCall
4175        | EffectiveChatOutputContract::StrictJsonSchemaContent
4176        | EffectiveChatOutputContract::JsonObjectContent
4177        | EffectiveChatOutputContract::Text => content.to_string(),
4178    }
4179}
4180
4181fn extract_json_object_text(text: &str) -> Option<String> {
4182    let text = strip_markdown_json_fence(text.trim());
4183    if serde_json::from_str::<serde_json::Value>(&text)
4184        .ok()
4185        .filter(|value| value.is_object())
4186        .is_some()
4187    {
4188        return Some(text.to_string());
4189    }
4190
4191    let start = text.find('{')?;
4192    let mut depth = 0usize;
4193    let mut in_string = false;
4194    let mut escaped = false;
4195    for (offset, ch) in text[start..].char_indices() {
4196        if in_string {
4197            if escaped {
4198                escaped = false;
4199            } else if ch == '\\' {
4200                escaped = true;
4201            } else if ch == '"' {
4202                in_string = false;
4203            }
4204            continue;
4205        }
4206        match ch {
4207            '"' => in_string = true,
4208            '{' => depth += 1,
4209            '}' => {
4210                depth = depth.saturating_sub(1);
4211                if depth == 0 {
4212                    let end = start + offset + ch.len_utf8();
4213                    let candidate = &text[start..end];
4214                    if serde_json::from_str::<serde_json::Value>(candidate)
4215                        .ok()
4216                        .filter(|value| value.is_object())
4217                        .is_some()
4218                    {
4219                        return Some(candidate.to_string());
4220                    }
4221                }
4222            }
4223            _ => {}
4224        }
4225    }
4226    None
4227}
4228
4229fn api_chat_request(
4230    request: &ChatCompletionsRequest,
4231    effective_tool_choice: Option<&ToolChoice>,
4232    tool_call_protocol: ferrum_types::ApiToolCallProtocol,
4233) -> ferrum_types::ApiChatRequest {
4234    ferrum_types::ApiChatRequest {
4235        messages: request.messages.iter().map(api_chat_message).collect(),
4236        tools: request
4237            .tools
4238            .as_deref()
4239            .unwrap_or_default()
4240            .iter()
4241            .map(api_tool)
4242            .collect(),
4243        tool_choice: effective_tool_choice.map(api_tool_choice),
4244        tool_call_protocol,
4245        legacy_functions: request
4246            .functions
4247            .as_deref()
4248            .unwrap_or_default()
4249            .iter()
4250            .map(api_function)
4251            .collect(),
4252        legacy_function_call: request.function_call.as_ref().map(api_function_call_choice),
4253        response_format: request.response_format.as_ref().map(api_response_format),
4254        stream_options: request.stream_options.as_ref().map(|opts| {
4255            ferrum_types::ApiStreamOptions {
4256                include_usage: opts.include_usage,
4257            }
4258        }),
4259    }
4260}
4261
4262fn api_chat_message(message: &ChatMessage) -> ferrum_types::ApiChatMessage {
4263    ferrum_types::ApiChatMessage {
4264        role: match message.role {
4265            MessageRole::System => ferrum_types::ApiMessageRole::System,
4266            MessageRole::User => ferrum_types::ApiMessageRole::User,
4267            MessageRole::Assistant => ferrum_types::ApiMessageRole::Assistant,
4268            MessageRole::Function => ferrum_types::ApiMessageRole::Function,
4269            MessageRole::Tool => ferrum_types::ApiMessageRole::Tool,
4270        },
4271        content: message.content.clone(),
4272        name: message.name.clone(),
4273        tool_calls: message
4274            .tool_calls
4275            .as_deref()
4276            .unwrap_or_default()
4277            .iter()
4278            .map(api_tool_call)
4279            .collect(),
4280        tool_call_id: message.tool_call_id.clone(),
4281        function_call: message.function_call.as_ref().map(api_function_call),
4282    }
4283}
4284
4285fn api_tool(tool: &ChatTool) -> ferrum_types::ApiTool {
4286    ferrum_types::ApiTool {
4287        tool_type: tool.tool_type.clone(),
4288        function: api_function(&tool.function),
4289    }
4290}
4291
4292fn api_function(function: &ChatFunction) -> ferrum_types::ApiFunction {
4293    ferrum_types::ApiFunction {
4294        name: function.name.clone(),
4295        description: function.description.clone(),
4296        parameters: function.parameters.clone(),
4297        strict: function.strict,
4298    }
4299}
4300
4301fn api_tool_choice(choice: &ToolChoice) -> ferrum_types::ApiToolChoice {
4302    match choice {
4303        ToolChoice::Mode(mode) => ferrum_types::ApiToolChoice::Mode(mode.clone()),
4304        ToolChoice::Function {
4305            tool_type,
4306            function,
4307        } => ferrum_types::ApiToolChoice::Function {
4308            tool_type: tool_type.clone(),
4309            function: ferrum_types::ApiToolChoiceFunction {
4310                name: function.name.clone(),
4311            },
4312        },
4313    }
4314}
4315
4316fn api_function_call_choice(choice: &FunctionCallChoice) -> ferrum_types::ApiFunctionCallChoice {
4317    match choice {
4318        FunctionCallChoice::Mode(mode) => ferrum_types::ApiFunctionCallChoice::Mode(mode.clone()),
4319        FunctionCallChoice::Function { name } => {
4320            ferrum_types::ApiFunctionCallChoice::Function { name: name.clone() }
4321        }
4322    }
4323}
4324
4325fn api_tool_call(tool_call: &ChatToolCall) -> ferrum_types::ApiToolCall {
4326    ferrum_types::ApiToolCall {
4327        id: tool_call.id.clone(),
4328        tool_type: tool_call.tool_type.clone(),
4329        function: api_function_call(&tool_call.function),
4330    }
4331}
4332
4333fn api_function_call(function_call: &ChatFunctionCall) -> ferrum_types::ApiFunctionCall {
4334    ferrum_types::ApiFunctionCall {
4335        name: function_call.name.clone(),
4336        arguments: function_call.arguments.clone(),
4337    }
4338}
4339
4340fn openai_chat_message_from_api(message: &ferrum_types::ApiChatMessage) -> ChatMessage {
4341    ChatMessage {
4342        role: openai_message_role_from_api(message.role),
4343        content: message.content.clone(),
4344        reasoning: None,
4345        name: message.name.clone(),
4346        tool_calls: if message.tool_calls.is_empty() {
4347            None
4348        } else {
4349            Some(
4350                message
4351                    .tool_calls
4352                    .iter()
4353                    .map(openai_tool_call_from_api)
4354                    .collect(),
4355            )
4356        },
4357        tool_call_id: message.tool_call_id.clone(),
4358        function_call: message
4359            .function_call
4360            .as_ref()
4361            .map(openai_function_call_from_api),
4362    }
4363}
4364
4365fn openai_message_role_from_api(role: ferrum_types::ApiMessageRole) -> MessageRole {
4366    match role {
4367        ferrum_types::ApiMessageRole::System => MessageRole::System,
4368        ferrum_types::ApiMessageRole::User => MessageRole::User,
4369        ferrum_types::ApiMessageRole::Assistant => MessageRole::Assistant,
4370        ferrum_types::ApiMessageRole::Function => MessageRole::Function,
4371        ferrum_types::ApiMessageRole::Tool => MessageRole::Tool,
4372    }
4373}
4374
4375fn openai_tool_call_from_api(tool_call: &ferrum_types::ApiToolCall) -> ChatToolCall {
4376    ChatToolCall {
4377        index: None,
4378        id: tool_call.id.clone(),
4379        tool_type: tool_call.tool_type.clone(),
4380        function: openai_function_call_from_api(&tool_call.function),
4381    }
4382}
4383
4384fn openai_tool_call_delta_from_api(
4385    index: usize,
4386    tool_call: &ferrum_types::ApiToolCall,
4387) -> ChatToolCall {
4388    ChatToolCall {
4389        index: Some(usize_to_u32_saturating(index)),
4390        id: tool_call.id.clone(),
4391        tool_type: tool_call.tool_type.clone(),
4392        function: openai_function_call_from_api(&tool_call.function),
4393    }
4394}
4395
4396fn openai_chat_delta_from_api(message: &ferrum_types::ApiChatMessage) -> ChatMessage {
4397    let mut delta = openai_chat_message_from_api(message);
4398    if !message.tool_calls.is_empty() {
4399        delta.tool_calls = Some(
4400            message
4401                .tool_calls
4402                .iter()
4403                .enumerate()
4404                .map(|(index, call)| openai_tool_call_delta_from_api(index, call))
4405                .collect(),
4406        );
4407    }
4408    delta
4409}
4410
4411fn openai_function_call_from_api(
4412    function_call: &ferrum_types::ApiFunctionCall,
4413) -> ChatFunctionCall {
4414    ChatFunctionCall {
4415        name: function_call.name.clone(),
4416        arguments: function_call.arguments.clone(),
4417    }
4418}
4419
4420fn api_response_format(format: &OpenAiResponseFormat) -> ferrum_types::ApiResponseFormat {
4421    ferrum_types::ApiResponseFormat {
4422        format_type: format.format_type.clone(),
4423        json_schema: format
4424            .json_schema
4425            .as_ref()
4426            .map(|schema| ferrum_types::ApiJsonSchema {
4427                name: schema.name.clone(),
4428                schema: schema.schema.clone().unwrap_or(serde_json::Value::Null),
4429                strict: schema.strict,
4430            }),
4431    }
4432}
4433
4434fn validate_chat_request(request: &ChatCompletionsRequest) -> std::result::Result<(), ServerError> {
4435    if request.messages.is_empty() {
4436        return Err(ServerError::invalid_request(
4437            "messages array must not be empty",
4438            Some("messages"),
4439        ));
4440    }
4441
4442    if let Some(n) = request.n {
4443        if n != 1 {
4444            return Err(ServerError::unsupported_feature(
4445                "only n=1 is supported for chat completions",
4446                Some("n"),
4447            ));
4448        }
4449    }
4450
4451    if request
4452        .logit_bias
4453        .as_ref()
4454        .is_some_and(|bias| !bias.is_empty())
4455    {
4456        return Err(ServerError::unsupported_feature(
4457            "logit_bias is not supported",
4458            Some("logit_bias"),
4459        ));
4460    }
4461    if request.logprobs.unwrap_or(false) {
4462        return Err(ServerError::unsupported_feature(
4463            "logprobs is not supported",
4464            Some("logprobs"),
4465        ));
4466    }
4467    if request.top_logprobs.unwrap_or(0) > 0 {
4468        return Err(ServerError::unsupported_feature(
4469            "top_logprobs is not supported",
4470            Some("top_logprobs"),
4471        ));
4472    }
4473
4474    if let Some(top_k) = request.top_k {
4475        if top_k < -1 {
4476            return Err(ServerError::invalid_request(
4477                "top_k must be -1, 0, or a positive integer",
4478                Some("top_k"),
4479            ));
4480        }
4481    }
4482    if let Some(min_p) = request.min_p {
4483        if !min_p.is_finite() || !(0.0..=1.0).contains(&min_p) {
4484            return Err(ServerError::invalid_request(
4485                "min_p must be in range [0, 1]",
4486                Some("min_p"),
4487            ));
4488        }
4489    }
4490    if let Some(repetition_penalty) = request.repetition_penalty {
4491        if !repetition_penalty.is_finite() || repetition_penalty <= 0.0 {
4492            return Err(ServerError::invalid_request(
4493                "repetition_penalty must be positive",
4494                Some("repetition_penalty"),
4495            ));
4496        }
4497    }
4498    if let Some(presence_penalty) = request.presence_penalty {
4499        if !presence_penalty.is_finite() || !(-2.0..=2.0).contains(&presence_penalty) {
4500            return Err(ServerError::invalid_request(
4501                "presence_penalty must be in range [-2, 2]",
4502                Some("presence_penalty"),
4503            ));
4504        }
4505    }
4506    if let Some(frequency_penalty) = request.frequency_penalty {
4507        if !frequency_penalty.is_finite() || !(-2.0..=2.0).contains(&frequency_penalty) {
4508            return Err(ServerError::invalid_request(
4509                "frequency_penalty must be in range [-2, 2]",
4510                Some("frequency_penalty"),
4511            ));
4512        }
4513    }
4514
4515    if request.stream_options.is_some() && !request.stream.unwrap_or(false) {
4516        return Err(ServerError::invalid_request(
4517            "stream_options is only valid when stream=true",
4518            Some("stream_options"),
4519        ));
4520    }
4521    ensure_response_format_supported(request)?;
4522
4523    if let Some(tools) = &request.tools {
4524        for tool in tools {
4525            if tool.tool_type != "function" {
4526                return Err(ServerError::unsupported_feature(
4527                    "only function tools are supported",
4528                    Some("tools"),
4529                ));
4530            }
4531        }
4532    }
4533
4534    if let Some(choice) = &request.tool_choice {
4535        match choice {
4536            ToolChoice::Mode(mode)
4537                if mode.eq_ignore_ascii_case("auto") || mode.eq_ignore_ascii_case("none") => {}
4538            ToolChoice::Mode(mode) if mode.eq_ignore_ascii_case("required") => {
4539                if request.tools.as_deref().unwrap_or_default().is_empty() {
4540                    return Err(ServerError::invalid_request(
4541                        "tool_choice=required requires at least one function tool",
4542                        Some("tool_choice"),
4543                    ));
4544                }
4545            }
4546            ToolChoice::Mode(_) => {
4547                return Err(ServerError::unsupported_feature(
4548                    "unsupported tool_choice mode",
4549                    Some("tool_choice"),
4550                ));
4551            }
4552            ToolChoice::Function {
4553                tool_type,
4554                function,
4555            } => {
4556                if tool_type != "function" {
4557                    return Err(ServerError::unsupported_feature(
4558                        "only function tool_choice is supported",
4559                        Some("tool_choice"),
4560                    ));
4561                }
4562                let declared = request
4563                    .tools
4564                    .as_deref()
4565                    .unwrap_or_default()
4566                    .iter()
4567                    .any(|tool| tool.function.name == function.name);
4568                if !declared {
4569                    return Err(ServerError::invalid_request(
4570                        "tool_choice selects a function that is not declared in tools",
4571                        Some("tool_choice"),
4572                    ));
4573                }
4574            }
4575        }
4576    }
4577
4578    if let Some(choice) = &request.function_call {
4579        match choice {
4580            FunctionCallChoice::Mode(mode)
4581                if mode.eq_ignore_ascii_case("auto") || mode.eq_ignore_ascii_case("none") => {}
4582            FunctionCallChoice::Mode(_) => {
4583                return Err(ServerError::unsupported_feature(
4584                    "unsupported function_call mode",
4585                    Some("function_call"),
4586                ));
4587            }
4588            FunctionCallChoice::Function { name } => {
4589                let declared = request
4590                    .functions
4591                    .as_deref()
4592                    .unwrap_or_default()
4593                    .iter()
4594                    .any(|function| function.name == *name);
4595                if !declared {
4596                    return Err(ServerError::invalid_request(
4597                        "function_call selects a function that is not declared in functions",
4598                        Some("function_call"),
4599                    ));
4600                }
4601            }
4602        }
4603    }
4604
4605    Ok(())
4606}
4607
4608fn tool_choice_required(request: &ChatCompletionsRequest) -> bool {
4609    match request.tool_choice.as_ref() {
4610        Some(ToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("required") => true,
4611        Some(ToolChoice::Function {
4612            tool_type,
4613            function,
4614        }) => {
4615            tool_type == "function"
4616                && request
4617                    .tools
4618                    .as_deref()
4619                    .unwrap_or_default()
4620                    .iter()
4621                    .any(|tool| tool.function.name == function.name)
4622        }
4623        _ => false,
4624    }
4625}
4626
4627fn openai_usage_from_token_usage(usage: &TokenUsage) -> Usage {
4628    let prompt_tokens = usize_to_u32_saturating(usage.prompt_tokens);
4629    let completion_tokens = usize_to_u32_saturating(usage.completion_tokens);
4630    let total_tokens = usize_to_u32_saturating(usage.total_tokens);
4631    Usage {
4632        prompt_tokens,
4633        completion_tokens,
4634        total_tokens,
4635    }
4636}
4637
4638fn usize_to_u32_saturating(value: usize) -> u32 {
4639    u32::try_from(value).unwrap_or(u32::MAX)
4640}
4641
4642fn ensure_response_format_supported(
4643    request: &ChatCompletionsRequest,
4644) -> std::result::Result<(), ServerError> {
4645    if let Some(rf) = &request.response_format {
4646        match rf.format_type.as_str() {
4647            "text" | "json_object" => {}
4648            "json_schema" => {
4649                let Some(schema_config) = rf.json_schema.as_ref() else {
4650                    return Err(ServerError::invalid_request(
4651                        "response_format.json_schema.schema is required",
4652                        Some("response_format.json_schema"),
4653                    ));
4654                };
4655                let Some(schema) = schema_config.schema.as_ref() else {
4656                    return Err(ServerError::invalid_request(
4657                        "response_format.json_schema.schema is required",
4658                        Some("response_format.json_schema"),
4659                    ));
4660                };
4661                if schema_config.strict.unwrap_or(false) {
4662                    compiled_json_schema_validator(schema).map_err(|reason| {
4663                        ServerError::invalid_request(
4664                            format!("unsupported strict json_schema: {reason}"),
4665                            Some("response_format.json_schema"),
4666                        )
4667                    })?;
4668                }
4669            }
4670            _ => {
4671                return Err(ServerError::invalid_request(
4672                    "unsupported response_format.type",
4673                    Some("response_format.type"),
4674                ));
4675            }
4676        }
4677    }
4678    Ok(())
4679}
4680
4681fn strict_json_schema_string(
4682    request: &ChatCompletionsRequest,
4683) -> std::result::Result<Option<String>, ServerError> {
4684    let Some(rf) = &request.response_format else {
4685        return Ok(None);
4686    };
4687    if rf.format_type != "json_schema" {
4688        return Ok(None);
4689    }
4690    let Some(schema) = &rf.json_schema else {
4691        return Err(ServerError::invalid_request(
4692            "response_format.json_schema.schema is required",
4693            Some("response_format.json_schema"),
4694        ));
4695    };
4696    let Some(schema_value) = schema.schema.as_ref() else {
4697        return Err(ServerError::invalid_request(
4698            "response_format.json_schema.schema is required",
4699            Some("response_format.json_schema"),
4700        ));
4701    };
4702    if !schema.strict.unwrap_or(false) {
4703        return Ok(None);
4704    }
4705    serde_json::to_string(schema_value).map(Some).map_err(|e| {
4706        ServerError::invalid_request(e.to_string(), Some("response_format.json_schema"))
4707    })
4708}
4709
4710fn validate_hard_structured_response(
4711    request: &ChatCompletionsRequest,
4712    content: &str,
4713    validated_chat_response: Option<&ferrum_types::ApiChatResponse>,
4714) -> std::result::Result<(), ServerError> {
4715    // Both terminal paths validate tool names, arguments and choice before
4716    // reaching this point. The final-answer schema applies to the content
4717    // branch; a tool call has its own independently validated argument schema.
4718    if validated_chat_response.is_some_and(|response| !response.message.tool_calls.is_empty()) {
4719        return Ok(());
4720    }
4721    // Validate the same content that the typed response will publish. A text
4722    // fallback may interpret marker-like strings inside valid JSON as framing.
4723    let content = validated_chat_response
4724        .map(|response| response.message.content.as_str())
4725        .unwrap_or(content);
4726    match EffectiveChatOutputContract::resolve(request) {
4727        EffectiveChatOutputContract::JsonObjectContent => {
4728            let value = serde_json::from_str::<serde_json::Value>(content).map_err(|error| {
4729                ServerError::InternalError(format!(
4730                    "model output did not satisfy response_format.json_object: invalid JSON: {error}"
4731                ))
4732            })?;
4733            if !value.is_object() {
4734                return Err(ServerError::InternalError(
4735                    "model output did not satisfy response_format.json_object: root must be an object"
4736                        .to_string(),
4737                ));
4738            }
4739            Ok(())
4740        }
4741        EffectiveChatOutputContract::StrictJsonSchemaContent => {
4742            let Some(schema_json) = strict_json_schema_string(request)? else {
4743                return Ok(());
4744            };
4745            let schema: serde_json::Value = serde_json::from_str(&schema_json).map_err(|e| {
4746                ServerError::InternalError(format!(
4747                    "strict json_schema could not be reconstructed after request validation: {e}"
4748                ))
4749            })?;
4750            validate_json_text_against_schema(&schema, content).map_err(|reason| {
4751                ServerError::InternalError(format!(
4752                    "model output did not satisfy response_format.json_schema.strict: {reason}"
4753                ))
4754            })
4755        }
4756        EffectiveChatOutputContract::RequiredToolCall
4757        | EffectiveChatOutputContract::BestEffortJsonSchemaContent
4758        | EffectiveChatOutputContract::Text => Ok(()),
4759    }
4760}
4761
4762fn structured_response_error_param(contract: EffectiveChatOutputContract) -> Option<&'static str> {
4763    match contract {
4764        EffectiveChatOutputContract::JsonObjectContent => Some("response_format"),
4765        EffectiveChatOutputContract::StrictJsonSchemaContent => Some("response_format.json_schema"),
4766        _ => None,
4767    }
4768}
4769
4770fn validate_structured_tool_response(
4771    request: &ChatCompletionsRequest,
4772    response: &ferrum_types::ApiChatResponse,
4773) -> std::result::Result<(), ServerError> {
4774    let required = tool_choice_required(request);
4775    if response.message.tool_calls.is_empty() {
4776        if required {
4777            return Err(ServerError::invalid_request(
4778                "model output did not satisfy required tool_choice",
4779                Some("tool_choice"),
4780            ));
4781        }
4782        return Ok(());
4783    }
4784
4785    if tool_choice_none(request.tool_choice.as_ref()) {
4786        return Err(ServerError::InternalError(
4787            "model emitted a tool call while tool_choice is 'none'".to_string(),
4788        ));
4789    }
4790
4791    if required {
4792        if !response.message.content.trim().is_empty() {
4793            return Err(ServerError::InternalError(
4794                "required tool response contained assistant content".to_string(),
4795            ));
4796        }
4797        if response.finish_reason.as_deref() != Some("tool_calls") {
4798            return Err(ServerError::InternalError(
4799                "required tool response did not finish with tool_calls".to_string(),
4800            ));
4801        }
4802    }
4803
4804    let tools = request.tools.as_deref().unwrap_or_default();
4805    for call in &response.message.tool_calls {
4806        if call.tool_type != "function" {
4807            return Err(ServerError::InternalError(format!(
4808                "model emitted unsupported tool call type '{}'",
4809                call.tool_type
4810            )));
4811        }
4812        let Some(tool) = tools
4813            .iter()
4814            .find(|tool| tool.tool_type == "function" && tool.function.name == call.function.name)
4815        else {
4816            return Err(ServerError::InternalError(format!(
4817                "model emitted undeclared tool call '{}'",
4818                call.function.name
4819            )));
4820        };
4821        if let Some(ToolChoice::Function {
4822            tool_type,
4823            function,
4824        }) = request.tool_choice.as_ref()
4825        {
4826            if tool_type != "function" || function.name != call.function.name {
4827                return Err(ServerError::InternalError(format!(
4828                    "model emitted tool '{}' instead of selected tool '{}'",
4829                    call.function.name, function.name
4830                )));
4831            }
4832        }
4833
4834        let arguments: serde_json::Value =
4835            serde_json::from_str(&call.function.arguments).map_err(|e| {
4836                ServerError::InternalError(format!(
4837                    "model emitted invalid JSON arguments for tool '{}': {e}",
4838                    call.function.name
4839                ))
4840            })?;
4841        if !arguments.is_object() {
4842            return Err(ServerError::InternalError(format!(
4843                "model emitted non-object arguments for tool '{}'",
4844                call.function.name
4845            )));
4846        }
4847        // Automatic Chat Completions tools are best effort unless the tool
4848        // opts into strict mode. Keep non-strict arguments intact so clients
4849        // can report validation errors through the normal tool-result turn.
4850        // Required/forced calls retain their existing hard output contract.
4851        if let Some(schema) = tool
4852            .function
4853            .parameters
4854            .as_ref()
4855            .filter(|_| required || tool.function.strict.unwrap_or(false))
4856        {
4857            validate_json_text_against_schema(schema, &call.function.arguments).map_err(
4858                |reason| {
4859                    ServerError::InternalError(format!(
4860                        "model arguments for tool '{}' did not satisfy its schema: {reason}",
4861                        call.function.name
4862                    ))
4863                },
4864            )?;
4865        }
4866    }
4867    Ok(())
4868}
4869
4870fn validate_json_text_against_schema(
4871    schema: &serde_json::Value,
4872    content: &str,
4873) -> std::result::Result<(), String> {
4874    let value = serde_json::from_str::<serde_json::Value>(content)
4875        .map_err(|e| format!("invalid JSON: {e}"))?;
4876    compiled_json_schema_validator(schema)?
4877        .validate(&value)
4878        .map_err(|error| error.to_string())
4879}
4880
4881fn compiled_json_schema_validator(
4882    schema: &serde_json::Value,
4883) -> std::result::Result<Arc<jsonschema::Validator>, String> {
4884    let cache_key = serde_json::to_string(schema)
4885        .map_err(|error| format!("could not serialize JSON Schema: {error}"))?;
4886    let cache = JSON_SCHEMA_VALIDATOR_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
4887    let mut validators = cache
4888        .lock()
4889        .map_err(|_| "JSON Schema validator cache lock was poisoned".to_string())?;
4890    if let Some(validator) = validators.get(&cache_key) {
4891        return Ok(Arc::clone(validator));
4892    }
4893
4894    let validator = Arc::new(
4895        jsonschema::validator_for(schema)
4896            .map_err(|error| format!("could not compile JSON Schema: {error}"))?,
4897    );
4898    if validators.len() >= MAX_CACHED_JSON_SCHEMA_VALIDATORS {
4899        validators.clear();
4900    }
4901    validators.insert(cache_key, Arc::clone(&validator));
4902    Ok(validator)
4903}
4904
4905fn stream_validation_error_message(error: ServerError) -> String {
4906    match error {
4907        ServerError::InternalError(message)
4908        | ServerError::NotImplemented(message)
4909        | ServerError::ServiceUnavailable(message)
4910        | ServerError::ContextLengthExceeded(message)
4911        | ServerError::InvalidRequest { message, .. }
4912        | ServerError::UnsupportedFeature { message, .. } => message,
4913    }
4914}
4915
4916fn server_error_from_ferrum_error(error: Error) -> ServerError {
4917    match error {
4918        Error::RequestValidation { message } => ServerError::invalid_request(message, None),
4919        error @ Error::ContextLengthExceeded { .. } => {
4920            ServerError::ContextLengthExceeded(error.to_string())
4921        }
4922        Error::ResourceExhausted { message } => ServerError::ServiceUnavailable(message),
4923        other => ServerError::InternalError(other.to_string()),
4924    }
4925}
4926
4927fn stream_error_payload(
4928    message: impl Into<String>,
4929    error_type: &str,
4930    param: Option<&str>,
4931) -> OpenAiError {
4932    OpenAiError {
4933        error: OpenAiErrorDetail {
4934            message: message.into(),
4935            error_type: error_type.to_string(),
4936            param: param.map(str::to_string),
4937            code: None,
4938        },
4939    }
4940}
4941
4942fn openai_error_sse_event(
4943    message: impl Into<String>,
4944    error_type: &str,
4945    param: Option<&str>,
4946) -> Event {
4947    Event::default()
4948        .json_data(&stream_error_payload(message, error_type, param))
4949        .unwrap_or_else(|_| Event::default().data("error"))
4950}
4951
4952fn convert_completion_request(request: &CompletionsRequest) -> InferenceRequest {
4953    let prompt = request
4954        .prompt
4955        .as_text()
4956        .expect("completion prompt validated before conversion");
4957    InferenceRequest {
4958        id: RequestId(Uuid::new_v4()),
4959        model_id: ModelId(request.model.clone()),
4960        prompt: prompt.to_string(),
4961        sampling_params: SamplingParams {
4962            max_tokens: request.max_tokens.unwrap_or(DEFAULT_COMPLETION_MAX_TOKENS) as usize,
4963            temperature: request.temperature.unwrap_or(DEFAULT_SAMPLING_TEMPERATURE),
4964            top_p: request.top_p.unwrap_or(DEFAULT_SAMPLING_TOP_P),
4965            top_k: None,
4966            repetition_penalty: 1.0,
4967            presence_penalty: 0.0,
4968            frequency_penalty: 0.0,
4969            stop_sequences: request.stop.clone().unwrap_or_default(),
4970            seed: None,
4971            min_p: None,
4972            tfs: None,
4973            typical_p: None,
4974            mirostat: None,
4975            response_format: ferrum_types::ResponseFormat::Text,
4976            structured_output_start: StructuredOutputStart::Immediate,
4977            response_completion_boundary: ResponseCompletionBoundary::Immediate,
4978            model_output_protocol: ferrum_types::ModelOutputProtocol::Text,
4979        },
4980        stream: request.stream.unwrap_or(false),
4981        priority: Priority::Normal,
4982        client_id: None,
4983        session_id: None,
4984        created_at: chrono::Utc::now(),
4985        api_request: Some(ferrum_types::ApiRequest::Completion(
4986            ferrum_types::ApiCompletionRequest {
4987                prompt: prompt.to_string(),
4988                response_format: None,
4989            },
4990        )),
4991        evidence_request: Default::default(),
4992        metadata: if request.max_tokens.is_none() {
4993            HashMap::from([(
4994                DEFAULT_MAX_TOKENS_METADATA_KEY.to_string(),
4995                serde_json::json!(true),
4996            )])
4997        } else {
4998            HashMap::new()
4999        },
5000    }
5001}
5002
5003fn resolve_request_model<'a>(
5004    registry: &'a ServedModelRegistry,
5005    request_model: &str,
5006    required_kind: ServedModelKind,
5007) -> std::result::Result<(ModelId, Option<&'a LoraAdapterModel>), ServerError> {
5008    if registry.is_empty() {
5009        return Ok((ModelId::new(request_model), None));
5010    }
5011    let entry = registry
5012        .resolve(request_model, required_kind)
5013        .ok_or_else(|| {
5014            ServerError::invalid_request(format!("unknown model: {request_model}"), Some("model"))
5015        })?;
5016    Ok((entry.engine_model_id().clone(), entry.adapter()))
5017}
5018
5019fn apply_served_model_resolution(
5020    inference_request: &mut InferenceRequest,
5021    engine_model_id: ModelId,
5022    adapter: Option<&LoraAdapterModel>,
5023) {
5024    inference_request.model_id = engine_model_id;
5025    if let Some(adapter) = adapter {
5026        inference_request.metadata.insert(
5027            "ferrum_lora_adapter".to_string(),
5028            serde_json::json!(adapter.name),
5029        );
5030        inference_request.metadata.insert(
5031            "ferrum_lora_model_id".to_string(),
5032            serde_json::json!(adapter.model_id),
5033        );
5034        inference_request.metadata.insert(
5035            "ferrum_lora_path".to_string(),
5036            serde_json::json!(adapter.path),
5037        );
5038    }
5039}
5040
5041async fn handle_completions_sync(
5042    state: AppState,
5043    openai_request: CompletionsRequest,
5044    inference_request: InferenceRequest,
5045) -> std::result::Result<Response, ServerError> {
5046    let engine = state.llm.clone().ok_or_else(|| {
5047        ServerError::ServiceUnavailable("LLM engine not loaded; completions unavailable".into())
5048    })?;
5049    match engine.infer(inference_request).await {
5050        Ok(output) => {
5051            let InferenceResponse {
5052                text: output_text,
5053                finish_reason,
5054                usage,
5055                api_response,
5056                ..
5057            } = output;
5058            let stop_sequences = openai_request.stop.clone().unwrap_or_default();
5059            let mut text = strip_after_stop(&output_text, &stop_sequences);
5060            let mut openai_finish_reason = finish_reason_to_string(&finish_reason);
5061            if let Some(ferrum_types::ApiResponse::Completion(completion_response)) =
5062                api_response.as_ref()
5063            {
5064                text = strip_after_stop(&completion_response.text, &stop_sequences);
5065                if let Some(reason) = &completion_response.finish_reason {
5066                    openai_finish_reason = reason.clone();
5067                }
5068            }
5069            let response = CompletionsResponse {
5070                id: Uuid::new_v4().to_string(),
5071                object: "text_completion".to_string(),
5072                created: chrono::Utc::now().timestamp() as u64,
5073                model: openai_request.model,
5074                choices: vec![CompletionChoice {
5075                    text,
5076                    index: 0,
5077                    finish_reason: Some(openai_finish_reason),
5078                }],
5079                usage: Some(openai_usage_from_token_usage(&usage)),
5080            };
5081            Ok(Json(response).into_response())
5082        }
5083        Err(e) => {
5084            error!("Completion generation failed: {}", e);
5085            Err(server_error_from_ferrum_error(e))
5086        }
5087    }
5088}
5089
5090async fn handle_completions_stream(
5091    state: AppState,
5092    openai_request: CompletionsRequest,
5093    inference_request: InferenceRequest,
5094) -> std::result::Result<Response, ServerError> {
5095    let (tx, rx) = mpsc::unbounded_channel::<std::result::Result<Event, axum::Error>>();
5096    let engine = state.llm.clone().ok_or_else(|| {
5097        ServerError::ServiceUnavailable("LLM engine not loaded; completions unavailable".into())
5098    })?;
5099    let request_id = Uuid::new_v4().to_string();
5100
5101    // Resolve startup rejection before sending SSE headers, as on the Chat
5102    // route. Once a stream exists, later failures remain SSE error events.
5103    let mut stream = engine.infer_stream(inference_request).await.map_err(|e| {
5104        error!("Failed to start completion stream: {}", e);
5105        server_error_from_ferrum_error(e)
5106    })?;
5107    tokio::spawn(async move {
5108        while let Some(result) = stream.next().await {
5109            match result {
5110                Ok(chunk) => {
5111                    let response_chunk = CompletionsResponse {
5112                        id: request_id.clone(),
5113                        object: "text_completion".to_string(),
5114                        created: chrono::Utc::now().timestamp() as u64,
5115                        model: openai_request.model.clone(),
5116                        choices: vec![CompletionChoice {
5117                            text: chunk.text.clone(),
5118                            index: 0,
5119                            finish_reason: chunk
5120                                .finish_reason
5121                                .as_ref()
5122                                .map(finish_reason_to_string),
5123                        }],
5124                        usage: None,
5125                    };
5126                    let event = Event::default()
5127                        .json_data(&response_chunk)
5128                        .unwrap_or_else(|_| Event::default().data("error"));
5129                    if tx.send(Ok(event)).is_err() {
5130                        break;
5131                    }
5132                    if chunk.finish_reason.is_some() {
5133                        if let Some(usage) = chunk.usage.as_ref().map(openai_usage_from_token_usage)
5134                        {
5135                            let final_chunk = CompletionsResponse {
5136                                id: request_id.clone(),
5137                                object: "text_completion".to_string(),
5138                                created: chrono::Utc::now().timestamp() as u64,
5139                                model: openai_request.model.clone(),
5140                                choices: vec![],
5141                                usage: Some(usage),
5142                            };
5143                            let event = Event::default()
5144                                .json_data(&final_chunk)
5145                                .unwrap_or_else(|_| Event::default().data("error"));
5146                            let _ = tx.send(Ok(event));
5147                        }
5148                        let _ = tx.send(Ok(Event::default().data("[DONE]")));
5149                        break;
5150                    }
5151                }
5152                Err(e) => {
5153                    error!("Completion stream generation error: {}", e);
5154                    let _ = tx.send(Ok(openai_error_sse_event(
5155                        e.to_string(),
5156                        "internal_server_error",
5157                        None,
5158                    )));
5159                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
5160                    break;
5161                }
5162            }
5163        }
5164    });
5165
5166    let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
5167    Ok(Sse::new(stream).into_response())
5168}
5169
5170/// Other handlers
5171async fn completions_handler(
5172    State(state): State<AppState>,
5173    request: std::result::Result<Json<CompletionsRequest>, JsonRejection>,
5174) -> std::result::Result<Response, ServerError> {
5175    let Json(request) = request.map_err(|e| {
5176        ServerError::invalid_request(format!("invalid completions request: {e}"), None)
5177    })?;
5178    validate_completion_request(&request)?;
5179    let (engine_model_id, lora_adapter) = resolve_request_model(
5180        &state.served_model_registry,
5181        &request.model,
5182        ServedModelKind::Llm,
5183    )?;
5184    let mut inference_request = convert_completion_request(&request);
5185    apply_served_model_resolution(&mut inference_request, engine_model_id, lora_adapter);
5186    if request.stream.unwrap_or(false) {
5187        handle_completions_stream(state, request, inference_request).await
5188    } else {
5189        handle_completions_sync(state, request, inference_request).await
5190    }
5191}
5192
5193fn validate_completion_request(
5194    request: &CompletionsRequest,
5195) -> std::result::Result<(), ServerError> {
5196    if request.prompt.as_text().is_none() {
5197        return Err(ServerError::invalid_request(
5198            "only string prompt is supported for completions",
5199            Some("prompt"),
5200        ));
5201    }
5202    if let Some(n) = request.n {
5203        if n != 1 {
5204            return Err(ServerError::unsupported_feature(
5205                "only n=1 is supported for completions",
5206                Some("n"),
5207            ));
5208        }
5209    }
5210    if request.logprobs.is_some() {
5211        return Err(ServerError::unsupported_feature(
5212            "logprobs is not supported for completions",
5213            Some("logprobs"),
5214        ));
5215    }
5216    if request
5217        .logit_bias
5218        .as_ref()
5219        .is_some_and(|bias| !bias.is_empty())
5220    {
5221        return Err(ServerError::unsupported_feature(
5222            "logit_bias is not supported",
5223            Some("logit_bias"),
5224        ));
5225    }
5226    Ok(())
5227}
5228
5229/// Embeddings handler — text and image embedding via OpenAI-compatible API.
5230async fn embeddings_handler(
5231    State(state): State<AppState>,
5232    request: std::result::Result<Json<EmbeddingsRequest>, JsonRejection>,
5233) -> std::result::Result<Response, ServerError> {
5234    let Json(request) = request.map_err(|e| {
5235        ServerError::invalid_request(format!("invalid embeddings request: {e}"), None)
5236    })?;
5237
5238    let span = span!(Level::INFO, "embeddings", model = %request.model);
5239    let _enter = span.enter();
5240
5241    validate_embeddings_request(&request)?;
5242    resolve_request_model(
5243        &state.served_model_registry,
5244        &request.model,
5245        ServedModelKind::Embedding,
5246    )?;
5247
5248    // Flatten input into individual items
5249    let items: Vec<EmbeddingItem> = match request.input {
5250        EmbeddingInput::Single(text) => vec![EmbeddingItem {
5251            text: Some(text),
5252            image: None,
5253        }],
5254        EmbeddingInput::Batch(texts) => texts
5255            .into_iter()
5256            .map(|t| EmbeddingItem {
5257                text: Some(t),
5258                image: None,
5259            })
5260            .collect(),
5261        EmbeddingInput::SingleObject(item) => vec![item],
5262        EmbeddingInput::BatchObjects(items) => items,
5263    };
5264
5265    if items.is_empty() {
5266        return Err(ServerError::invalid_request(
5267            "input must not be empty",
5268            Some("input"),
5269        ));
5270    }
5271
5272    let mut data = Vec::with_capacity(items.len());
5273    let mut total_tokens = 0u32;
5274
5275    let engine = state.embed.as_ref().ok_or_else(|| {
5276        ServerError::NotImplemented("Embed engine not loaded; embeddings unavailable".into())
5277    })?;
5278    for (idx, item) in items.iter().enumerate() {
5279        let embedding = if let Some(ref image) = item.image {
5280            engine
5281                .embed_image(image)
5282                .await
5283                .map_err(|e| ServerError::InternalError(format!("embed_image: {e}")))?
5284        } else if let Some(ref text) = item.text {
5285            total_tokens += text.len() as u32;
5286            engine
5287                .embed_text(text)
5288                .await
5289                .map_err(|e| ServerError::InternalError(format!("embed_text: {e}")))?
5290        } else {
5291            return Err(ServerError::invalid_request(
5292                "each input item must have either text or image",
5293                Some("input"),
5294            ));
5295        };
5296
5297        data.push(EmbeddingData {
5298            object: "embedding".to_string(),
5299            embedding,
5300            index: idx,
5301        });
5302    }
5303
5304    let response = EmbeddingsResponse {
5305        object: "list".to_string(),
5306        data,
5307        model: request.model,
5308        usage: EmbeddingUsage {
5309            prompt_tokens: total_tokens,
5310            total_tokens,
5311        },
5312    };
5313
5314    Ok(Json(response).into_response())
5315}
5316
5317fn validate_embeddings_request(
5318    request: &EmbeddingsRequest,
5319) -> std::result::Result<(), ServerError> {
5320    if let Some(format) = request.encoding_format.as_deref() {
5321        if !format.eq_ignore_ascii_case("float") {
5322            return Err(ServerError::unsupported_feature(
5323                "only encoding_format=float is supported for embeddings",
5324                Some("encoding_format"),
5325            ));
5326        }
5327    }
5328    Ok(())
5329}
5330
5331/// Audio transcription handler (OpenAI-compatible multipart form).
5332async fn transcriptions_handler(
5333    State(state): State<AppState>,
5334    multipart: std::result::Result<axum::extract::Multipart, MultipartRejection>,
5335) -> std::result::Result<Response, ServerError> {
5336    let mut multipart = multipart.map_err(|e| {
5337        ServerError::invalid_request(format!("invalid transcriptions request: {e}"), None)
5338    })?;
5339
5340    let span = span!(Level::INFO, "transcription");
5341    let _enter = span.enter();
5342
5343    let mut file_data: Option<Vec<u8>> = None;
5344    let mut language: Option<String> = None;
5345    let mut response_format: Option<String> = None;
5346
5347    while let Some(field) = multipart
5348        .next_field()
5349        .await
5350        .map_err(|e| ServerError::invalid_request(format!("multipart: {e}"), None))?
5351    {
5352        let name = field.name().unwrap_or("").to_string();
5353        match name.as_str() {
5354            "file" => {
5355                file_data = Some(
5356                    field
5357                        .bytes()
5358                        .await
5359                        .map_err(|e| {
5360                            ServerError::invalid_request(format!("read file: {e}"), Some("file"))
5361                        })?
5362                        .to_vec(),
5363                );
5364            }
5365            "language" => {
5366                language = field.text().await.ok().filter(|s| !s.is_empty());
5367            }
5368            "response_format" => {
5369                response_format = field.text().await.ok().filter(|s| !s.is_empty());
5370            }
5371            _ => {} // ignore model and other optional multipart fields for now
5372        }
5373    }
5374
5375    validate_transcription_response_format(response_format.as_deref())?;
5376
5377    let data = file_data
5378        .ok_or_else(|| ServerError::invalid_request("missing file field", Some("file")))?;
5379
5380    let engine = state.transcribe.as_ref().ok_or_else(|| {
5381        ServerError::NotImplemented("Transcribe engine not loaded; ASR unavailable".into())
5382    })?;
5383    let text = engine
5384        .transcribe_bytes(&data, language.as_deref())
5385        .await
5386        .map_err(|e| ServerError::InternalError(format!("transcribe: {e}")))?;
5387
5388    Ok(Json(TranscriptionResponse { text }).into_response())
5389}
5390
5391fn validate_transcription_response_format(
5392    response_format: Option<&str>,
5393) -> std::result::Result<(), ServerError> {
5394    if let Some(format) = response_format {
5395        if !format.eq_ignore_ascii_case("json") {
5396            return Err(ServerError::unsupported_feature(
5397                "only response_format=json is supported for transcriptions",
5398                Some("response_format"),
5399            ));
5400        }
5401    }
5402    Ok(())
5403}
5404
5405/// TTS speech synthesis handler (OpenAI-compatible /v1/audio/speech)
5406async fn speech_handler(
5407    State(state): State<AppState>,
5408    request: std::result::Result<Json<SpeechRequest>, JsonRejection>,
5409) -> std::result::Result<Response, ServerError> {
5410    let Json(request) = request
5411        .map_err(|e| ServerError::invalid_request(format!("invalid speech request: {e}"), None))?;
5412
5413    let response_format = speech_output_format(&request)?;
5414    resolve_request_model(
5415        &state.served_model_registry,
5416        &request.model,
5417        ServedModelKind::Speech,
5418    )?;
5419
5420    let span = span!(Level::INFO, "speech");
5421    let _guard = span.enter();
5422
5423    let language = if request.language.is_empty() || request.language == "auto" {
5424        None
5425    } else {
5426        Some(request.language.as_str())
5427    };
5428
5429    let chunk_frames = 10usize;
5430    let tts = state.tts.as_ref().ok_or_else(|| {
5431        ServerError::NotImplemented("TTS engine not loaded; speech unavailable".into())
5432    })?;
5433    let sample_rate = tts.tts_sample_rate();
5434
5435    if request.stream {
5436        // Streaming: chunked transfer encoding with WAV audio
5437        let (tx, rx) =
5438            mpsc::unbounded_channel::<std::result::Result<axum::body::Bytes, std::io::Error>>();
5439
5440        let engine = tts.clone();
5441        let text = request.input.clone();
5442        let lang = request.language.clone();
5443
5444        tokio::task::spawn_blocking(move || {
5445            let lang_opt = if lang.is_empty() || lang == "auto" {
5446                None
5447            } else {
5448                Some(lang.as_str())
5449            };
5450            let rt = tokio::runtime::Handle::current();
5451
5452            match rt.block_on(engine.synthesize_speech(&text, lang_opt, chunk_frames)) {
5453                Ok(chunks) => {
5454                    for chunk in &chunks {
5455                        let audio_bytes = encode_speech_audio(chunk, sample_rate, response_format);
5456                        let _ = tx.send(Ok(axum::body::Bytes::from(audio_bytes)));
5457                    }
5458                }
5459                Err(e) => {
5460                    error!("TTS error: {e}");
5461                }
5462            }
5463        });
5464
5465        let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
5466        let body = axum::body::Body::from_stream(stream);
5467        Ok(Response::builder()
5468            .status(200)
5469            .header("content-type", speech_content_type(response_format))
5470            .header("transfer-encoding", "chunked")
5471            .body(body)
5472            .unwrap())
5473    } else {
5474        // Non-streaming: return complete WAV
5475        let chunks = tts
5476            .synthesize_speech(&request.input, language, chunk_frames)
5477            .await
5478            .map_err(|e| ServerError::InternalError(format!("TTS: {e}")))?;
5479
5480        let all_samples: Vec<f32> = chunks.into_iter().flatten().collect();
5481        let audio_bytes = encode_speech_audio(&all_samples, sample_rate, response_format);
5482
5483        Ok(Response::builder()
5484            .status(200)
5485            .header("content-type", speech_content_type(response_format))
5486            .header("content-length", audio_bytes.len().to_string())
5487            .body(axum::body::Body::from(audio_bytes))
5488            .unwrap())
5489    }
5490}
5491
5492#[derive(Clone, Copy)]
5493enum SpeechOutputFormat {
5494    Wav,
5495    Pcm,
5496}
5497
5498fn speech_output_format(
5499    request: &SpeechRequest,
5500) -> std::result::Result<SpeechOutputFormat, ServerError> {
5501    if request.response_format.eq_ignore_ascii_case("wav") {
5502        Ok(SpeechOutputFormat::Wav)
5503    } else if request.response_format.eq_ignore_ascii_case("pcm") {
5504        Ok(SpeechOutputFormat::Pcm)
5505    } else {
5506        Err(ServerError::unsupported_feature(
5507            "only response_format=wav or response_format=pcm is supported for speech",
5508            Some("response_format"),
5509        ))
5510    }
5511}
5512
5513fn speech_content_type(format: SpeechOutputFormat) -> &'static str {
5514    match format {
5515        SpeechOutputFormat::Wav => "audio/wav",
5516        SpeechOutputFormat::Pcm => "audio/pcm",
5517    }
5518}
5519
5520fn encode_speech_audio(samples: &[f32], sample_rate: u32, format: SpeechOutputFormat) -> Vec<u8> {
5521    match format {
5522        SpeechOutputFormat::Wav => pcm_to_wav_bytes(samples, sample_rate),
5523        SpeechOutputFormat::Pcm => pcm_to_s16le_bytes(samples),
5524    }
5525}
5526
5527/// Convert PCM f32 samples to WAV bytes (16-bit, mono).
5528fn pcm_to_wav_bytes(samples: &[f32], sample_rate: u32) -> Vec<u8> {
5529    let num_samples = samples.len();
5530    let data_size = num_samples * 2; // 16-bit = 2 bytes per sample
5531    let file_size = 44 + data_size;
5532
5533    let mut buf = Vec::with_capacity(file_size);
5534    // RIFF header
5535    buf.extend_from_slice(b"RIFF");
5536    buf.extend_from_slice(&((file_size - 8) as u32).to_le_bytes());
5537    buf.extend_from_slice(b"WAVE");
5538    // fmt chunk
5539    buf.extend_from_slice(b"fmt ");
5540    buf.extend_from_slice(&16u32.to_le_bytes()); // chunk size
5541    buf.extend_from_slice(&1u16.to_le_bytes()); // PCM
5542    buf.extend_from_slice(&1u16.to_le_bytes()); // mono
5543    buf.extend_from_slice(&sample_rate.to_le_bytes());
5544    buf.extend_from_slice(&(sample_rate * 2).to_le_bytes()); // byte rate
5545    buf.extend_from_slice(&2u16.to_le_bytes()); // block align
5546    buf.extend_from_slice(&16u16.to_le_bytes()); // bits per sample
5547                                                 // data chunk
5548    buf.extend_from_slice(b"data");
5549    buf.extend_from_slice(&(data_size as u32).to_le_bytes());
5550    buf.extend_from_slice(&pcm_to_s16le_bytes(samples));
5551    buf
5552}
5553
5554fn pcm_to_s16le_bytes(samples: &[f32]) -> Vec<u8> {
5555    let mut buf = Vec::with_capacity(samples.len() * 2);
5556    for &s in samples {
5557        let i16_val = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
5558        buf.extend_from_slice(&i16_val.to_le_bytes());
5559    }
5560    buf
5561}
5562
5563async fn models_handler(
5564    State(state): State<AppState>,
5565) -> std::result::Result<Response, ServerError> {
5566    let now = chrono::Utc::now().timestamp() as u64;
5567    let reasoning = state.prompt_template.as_ref().and_then(|template| {
5568        let supported_efforts = template
5569            .reasoning_effort_support
5570            .declared_efforts()
5571            .map(|efforts| efforts.iter().copied().collect());
5572        let thinking =
5573            template
5574                .supports_thinking_control()
5575                .then(|| crate::openai::ModelThinkingCapability {
5576                    default_enabled: state
5577                        .default_enable_thinking
5578                        .unwrap_or(template.reasoning_default_enabled),
5579                });
5580        (supported_efforts.is_some() || thinking.is_some()).then_some(
5581            crate::openai::ModelReasoningCapabilities {
5582                supported_efforts,
5583                thinking,
5584            },
5585        )
5586    });
5587    let data = state
5588        .served_model_registry
5589        .entries()
5590        .iter()
5591        .map(|entry| crate::openai::ModelInfo {
5592            id: entry.public_name().to_string(),
5593            object: "model".to_string(),
5594            created: now,
5595            owned_by: "ferrum".to_string(),
5596            max_model_len: match entry.kind() {
5597                ServedModelKind::Llm => state.llm.as_ref().and_then(|llm| llm.context_capacity()),
5598                _ => None,
5599            },
5600            reasoning: if entry.kind() == ServedModelKind::Llm
5601                && state
5602                    .llm
5603                    .as_ref()
5604                    .is_some_and(|llm| &llm.config().model.model_id == entry.engine_model_id())
5605            {
5606                reasoning.clone()
5607            } else {
5608                None
5609            },
5610            modalities: entry
5611                .kind()
5612                .modalities()
5613                .iter()
5614                .map(ToString::to_string)
5615                .collect(),
5616            permission: vec![],
5617            root: entry.parent_public_name().map(ToString::to_string),
5618            parent: entry.parent_public_name().map(ToString::to_string),
5619        })
5620        .collect();
5621
5622    let models = ModelListResponse {
5623        object: "list".to_string(),
5624        data,
5625    };
5626
5627    Ok(Json(models).into_response())
5628}
5629
5630async fn health_handler(
5631    State(state): State<AppState>,
5632) -> std::result::Result<Response, ServerError> {
5633    let engine_status = state.status().await;
5634    let scheduler_metrics = state.metrics();
5635    let runtime_config = RuntimeConfigSnapshot::capture_current();
5636    let cache_policy = CachePolicy::current();
5637    let engine_cache = state
5638        .llm
5639        .as_ref()
5640        .and_then(|engine| engine.cache_metrics_snapshot());
5641    let execution_attribution = state
5642        .llm
5643        .as_ref()
5644        .and_then(|engine| engine.execution_attribution_snapshot());
5645    let engine_lora = state
5646        .llm
5647        .as_ref()
5648        .and_then(|engine| engine.lora_metrics_snapshot());
5649    let auto_config = auto_config_health_value(state.auto_config.as_ref());
5650    let runtime_admission = match state.llm.as_ref() {
5651        Some(engine) => engine.admission_snapshot(),
5652        None => Ok(None),
5653    };
5654    let (runtime_admission_snapshot, runtime_admission_error) = match &runtime_admission {
5655        Ok(snapshot) => (snapshot.as_ref(), None),
5656        Err(error) => (None, Some(error.to_string())),
5657    };
5658    let admission = admission_health_json(
5659        &engine_status,
5660        &scheduler_metrics,
5661        &auto_config,
5662        runtime_admission_snapshot,
5663        runtime_admission_error.as_deref(),
5664    );
5665
5666    let health = serde_json::json!({
5667        "status": if runtime_admission_error.is_some() { "unhealthy" } else { "healthy" },
5668        "reasoning_protocol": state.prompt_template.as_deref().map(ModelChatTemplate::reasoning_capability).unwrap_or_default(),
5669        "timestamp": chrono::Utc::now().to_rfc3339(),
5670        "version": env!("CARGO_PKG_VERSION"),
5671        "engine": {
5672            "active_requests": engine_status.active_requests,
5673            "queued_requests": engine_status.queued_requests,
5674        },
5675        "scheduler": {
5676            "total_requests": scheduler_metrics.total_requests,
5677            "successful_requests": scheduler_metrics.successful_requests,
5678            "failed_requests": scheduler_metrics.failed_requests,
5679            "throughput_rps": scheduler_metrics.throughput_rps,
5680            "avg_wait_time_ms": scheduler_metrics.queue_metrics.avg_queue_wait_time_ms,
5681            "scheduling_time_ms": scheduler_metrics.performance_breakdown.scheduling_time_ms,
5682            "model_execution_time_ms": scheduler_metrics
5683                .performance_breakdown
5684                .model_execution_time_ms,
5685            "iteration_lock_wait_time_ms": scheduler_metrics
5686                .performance_breakdown
5687                .other_overhead_time_ms,
5688        },
5689        "config": runtime_config,
5690        "auto_config": auto_config,
5691        "admission": admission,
5692        "numerical_execution": engine_cache.as_ref().and_then(|snapshot| snapshot.get("numerical_execution")),
5693        "kv_storage": engine_cache.as_ref().and_then(|snapshot| snapshot.get("kv_storage")),
5694        "cache": state.cache.health_json(&cache_policy, engine_cache.as_ref()),
5695        "execution_attribution": execution_attribution,
5696        "lora": engine_lora.unwrap_or_else(|| serde_json::json!({
5697            "enabled": state.served_model_registry.adapter_count() > 0,
5698            "adapter_count": state.served_model_registry.adapter_count() as u64,
5699            "active_cache_bindings": 0u64,
5700            "projection_applications": 0u64,
5701            "position": "startup-routing",
5702            "source": "server-lora-registry",
5703        })),
5704    });
5705
5706    Ok(Json(health).into_response())
5707}
5708
5709/// Prometheus metrics endpoint — returns metrics in Prometheus text format.
5710async fn metrics_handler(
5711    State(state): State<AppState>,
5712) -> std::result::Result<Response, ServerError> {
5713    let mut body = match PROM_HANDLE.get() {
5714        Some(handle) => handle.render(),
5715        None => "# Prometheus recorder not initialized\n".to_string(),
5716    };
5717    if !body.ends_with('\n') {
5718        body.push('\n');
5719    }
5720    let engine_cache = state
5721        .llm
5722        .as_ref()
5723        .and_then(|engine| engine.cache_metrics_snapshot());
5724    body.push_str(&state.cache.prometheus_metrics(engine_cache.as_ref()));
5725    let engine_status = state.status().await;
5726    let scheduler_metrics = state.metrics();
5727    let auto_config = auto_config_health_value(state.auto_config.as_ref());
5728    let runtime_admission = match state.llm.as_ref() {
5729        Some(engine) => engine.admission_snapshot(),
5730        None => Ok(None),
5731    };
5732    let (runtime_admission_snapshot, runtime_admission_error) = match &runtime_admission {
5733        Ok(snapshot) => (snapshot.as_ref(), None),
5734        Err(error) => (None, Some(error.to_string())),
5735    };
5736    let admission = admission_health_json(
5737        &engine_status,
5738        &scheduler_metrics,
5739        &auto_config,
5740        runtime_admission_snapshot,
5741        runtime_admission_error.as_deref(),
5742    );
5743    body.push_str(&admission_prometheus_metrics(&admission));
5744
5745    Ok((
5746        [(
5747            axum::http::header::CONTENT_TYPE,
5748            "text/plain; version=0.0.4; charset=utf-8",
5749        )],
5750        body,
5751    )
5752        .into_response())
5753}
5754
5755async fn root_handler() -> std::result::Result<Response, ServerError> {
5756    let info = serde_json::json!({
5757        "name": "Ferrum Inference Server",
5758        "version": env!("CARGO_PKG_VERSION"),
5759        "api_version": "v1",
5760        "status": "running"
5761    });
5762
5763    Ok(Json(info).into_response())
5764}
5765
5766/// Server error type for HTTP responses
5767#[derive(Debug)]
5768enum ServerError {
5769    InvalidRequest {
5770        message: String,
5771        param: Option<String>,
5772    },
5773    UnsupportedFeature {
5774        message: String,
5775        param: Option<String>,
5776    },
5777    InternalError(String),
5778    ContextLengthExceeded(String),
5779    NotImplemented(String),
5780    ServiceUnavailable(String),
5781}
5782
5783impl ServerError {
5784    fn invalid_request(message: impl Into<String>, param: Option<&str>) -> Self {
5785        Self::InvalidRequest {
5786            message: message.into(),
5787            param: param.map(str::to_string),
5788        }
5789    }
5790
5791    fn unsupported_feature(message: impl Into<String>, param: Option<&str>) -> Self {
5792        Self::UnsupportedFeature {
5793            message: message.into(),
5794            param: param.map(str::to_string),
5795        }
5796    }
5797}
5798
5799impl IntoResponse for ServerError {
5800    fn into_response(self) -> Response {
5801        let code = matches!(&self, ServerError::ContextLengthExceeded(_))
5802            .then(|| "context_length_exceeded".to_owned());
5803        let (status, message, error_type, param) = match self {
5804            ServerError::ContextLengthExceeded(message) => (
5805                AxumStatusCode::BAD_REQUEST,
5806                message,
5807                "invalid_request_error",
5808                None,
5809            ),
5810            ServerError::InvalidRequest { message, param } => (
5811                AxumStatusCode::BAD_REQUEST,
5812                message,
5813                "invalid_request_error",
5814                param,
5815            ),
5816            ServerError::UnsupportedFeature { message, param } => (
5817                AxumStatusCode::BAD_REQUEST,
5818                message,
5819                "invalid_request_error",
5820                param,
5821            ),
5822            ServerError::InternalError(msg) => (
5823                AxumStatusCode::INTERNAL_SERVER_ERROR,
5824                msg,
5825                "internal_server_error",
5826                None,
5827            ),
5828            ServerError::NotImplemented(msg) => (
5829                AxumStatusCode::SERVICE_UNAVAILABLE,
5830                msg,
5831                "service_unavailable_error",
5832                None,
5833            ),
5834            ServerError::ServiceUnavailable(msg) => (
5835                AxumStatusCode::SERVICE_UNAVAILABLE,
5836                msg,
5837                "service_unavailable_error",
5838                None,
5839            ),
5840        };
5841
5842        let error = OpenAiError {
5843            error: OpenAiErrorDetail {
5844                message,
5845                error_type: error_type.to_string(),
5846                param,
5847                code,
5848            },
5849        };
5850
5851        (status, Json(error)).into_response()
5852    }
5853}
5854
5855impl std::fmt::Display for MessageRole {
5856    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5857        match self {
5858            MessageRole::System => write!(f, "system"),
5859            MessageRole::User => write!(f, "user"),
5860            MessageRole::Assistant => write!(f, "assistant"),
5861            MessageRole::Function => write!(f, "function"),
5862            MessageRole::Tool => write!(f, "tool"),
5863        }
5864    }
5865}
5866
5867/// Strip model output at the first user-supplied stop sequence.
5868/// OpenAI-compatible `stop` strings are generation boundaries and must not
5869/// be returned to the caller, even if the model continued after the boundary.
5870fn strip_after_stop(text: &str, stops: &[String]) -> String {
5871    let mut first: Option<usize> = None;
5872    for stop in stops {
5873        if stop.is_empty() {
5874            continue;
5875        }
5876        if let Some(idx) = text.find(stop.as_str()) {
5877            first = Some(first.map_or(idx, |current| current.min(idx)));
5878        }
5879    }
5880    match first {
5881        Some(idx) => text[..idx].to_string(),
5882        None => text.to_string(),
5883    }
5884}
5885
5886/// Compatibility cleanup for explicitly best-effort structured output.
5887/// Hard `json_object` and strict schema contracts never call this helper.
5888fn strip_markdown_json_fence(text: &str) -> String {
5889    let trimmed = text.trim();
5890    // Try the most specific marker first.
5891    for prefix in ["```json\n", "```json", "```\n", "```"] {
5892        if let Some(rest) = trimmed.strip_prefix(prefix) {
5893            if let Some(inner) = rest.strip_suffix("```") {
5894                return inner.trim().to_string();
5895            }
5896        }
5897    }
5898    text.to_string()
5899}
5900
5901/// Convert FinishReason to OpenAI API string
5902fn finish_reason_to_string(reason: &FinishReason) -> String {
5903    match reason {
5904        FinishReason::Length => "length".to_string(),
5905        FinishReason::Stop => "stop".to_string(),
5906        FinishReason::EOS => "stop".to_string(),
5907        FinishReason::Cancelled => "cancelled".to_string(),
5908        FinishReason::Error => "error".to_string(),
5909        FinishReason::ContentFilter => "content_filter".to_string(),
5910    }
5911}
5912
5913#[cfg(test)]
5914mod tests {
5915    mod auto_tools_json;
5916    mod engine_stop_contract;
5917    mod gemma_thought;
5918    mod harmony_stops;
5919    mod model_reasoning_metadata;
5920    mod native_tool_stream;
5921    mod reasoning_controls;
5922    mod tool_argument_strictness;
5923    mod tool_length;
5924    use super::*;
5925    use async_trait::async_trait;
5926    use axum::{
5927        body::{to_bytes, Body},
5928        http::{header, Request},
5929        response::Response,
5930    };
5931    use ferrum_interfaces::engine::{
5932        EmbedEngine, InferenceEngine, LlmInferenceEngine, TranscribeEngine, TtsEngine,
5933    };
5934    use ferrum_types::{
5935        has_unclosed_thinking_block, parse_reasoning_response_started_in_think, EngineConfig,
5936        EngineMetrics, EngineStatus, EngineTokenTimingEvidence, FinishReason,
5937        HealthStatus as EngineHealthStatus, InferenceRequest, InferenceResponse, MemoryUsage,
5938        ModelId, StreamChunk, TokenId, TokenUsage,
5939    };
5940    use futures::{stream, Stream};
5941    use serde_json::{json, Value};
5942    use std::{
5943        collections::HashMap,
5944        pin::Pin,
5945        sync::{atomic::AtomicUsize, Arc, Mutex},
5946    };
5947    use tower::ServiceExt;
5948
5949    #[test]
5950    fn strip_after_stop_removes_first_boundary() {
5951        assert_eq!(
5952            strip_after_stop(
5953                "KS0214Z\nS0225\nEND0214Z0214Z\nS0225\n",
5954                &["END0214Z".to_string()]
5955            ),
5956            "KS0214Z\nS0225\n"
5957        );
5958    }
5959
5960    #[test]
5961    fn gpt_oss_harmony_final_is_split_into_reasoning_and_visible_content() {
5962        let parsed = parse_chat_model_output(
5963            ModelOutputProtocol::HarmonyGptOss,
5964            "<|channel|>analysis<|message|>Reason.<|end|>\
5965             <|start|>assistant<|channel|>final<|message|>Answer.<|return|>",
5966            false,
5967            FinishReason::Stop,
5968        )
5969        .unwrap();
5970        assert_eq!(parsed.visible.content, "Answer.");
5971        assert_eq!(parsed.visible.reasoning.as_deref(), Some("Reason."));
5972        assert!(parsed.harmony_response.is_none());
5973    }
5974
5975    #[test]
5976    fn gpt_oss_harmony_tool_call_becomes_openai_structured_response() {
5977        let parsed = parse_chat_model_output(
5978            ModelOutputProtocol::HarmonyGptOss,
5979            "<|channel|>analysis<|message|>Need weather.<|end|>\
5980             <|start|>assistant<|channel|>commentary to=functions.weather\
5981             <|constrain|>json<|message|>{\"city\":\"Paris\"}<|call|>",
5982            false,
5983            FinishReason::Stop,
5984        )
5985        .unwrap();
5986        let response = parsed.harmony_response.unwrap();
5987        assert_eq!(response.finish_reason.as_deref(), Some("tool_calls"));
5988        assert_eq!(response.message.tool_calls.len(), 1);
5989        assert_eq!(response.message.tool_calls[0].function.name, "weather");
5990        assert_eq!(
5991            response.message.tool_calls[0].function.arguments,
5992            "{\"city\":\"Paris\"}"
5993        );
5994        assert!(response.message.tool_calls[0].id.starts_with("call_"));
5995    }
5996
5997    #[test]
5998    fn gpt_oss_harmony_accepts_missing_text_terminal_only_for_explicit_truncation() {
5999        let output = "<|channel|>analysis<|message|>Still reasoning";
6000        for finish_reason in [FinishReason::Stop, FinishReason::Length] {
6001            let parsed = parse_chat_model_output(
6002                ModelOutputProtocol::HarmonyGptOss,
6003                output,
6004                false,
6005                finish_reason,
6006            )
6007            .unwrap();
6008            assert_eq!(parsed.visible.reasoning.as_deref(), Some("Still reasoning"));
6009            assert!(parsed.visible.content.is_empty());
6010        }
6011        for finish_reason in [
6012            FinishReason::EOS,
6013            FinishReason::Cancelled,
6014            FinishReason::Error,
6015            FinishReason::ContentFilter,
6016        ] {
6017            assert!(parse_chat_model_output(
6018                ModelOutputProtocol::HarmonyGptOss,
6019                output,
6020                false,
6021                finish_reason,
6022            )
6023            .is_err());
6024        }
6025    }
6026
6027    #[tokio::test]
6028    async fn stop_drains_running_server_and_shuts_down_loaded_engine_once() {
6029        let engine = Arc::new(StubLlm::new("ok"));
6030        let server = Arc::new(AxumServer::from_llm(engine.clone()));
6031        let config = ServerConfig {
6032            host: "127.0.0.1".to_string(),
6033            port: 0,
6034            ..ServerConfig::default()
6035        };
6036        let server_task = {
6037            let server = Arc::clone(&server);
6038            tokio::spawn(async move { server.start(&config).await })
6039        };
6040        tokio::time::timeout(std::time::Duration::from_secs(1), async {
6041            while !server.is_running() {
6042                tokio::task::yield_now().await;
6043            }
6044        })
6045        .await
6046        .unwrap();
6047
6048        server
6049            .stop(std::time::Duration::from_secs(1))
6050            .await
6051            .unwrap();
6052        server
6053            .stop(std::time::Duration::from_secs(1))
6054            .await
6055            .unwrap();
6056        server_task.await.unwrap().unwrap();
6057
6058        assert_eq!(engine.shutdown_count.load(Ordering::Acquire), 1);
6059        assert!(!server.is_running());
6060    }
6061
6062    struct StubLlm {
6063        config: EngineConfig,
6064        resource_authority: ExecutionResourceAuthority,
6065        context_capacity: Option<usize>,
6066        text: String,
6067        stream_chunks: Option<Vec<String>>,
6068        stream_final_chunk_separate: bool,
6069        stream_tail_without_token: bool,
6070        stream_usage: Option<TokenUsage>,
6071        api_response: Option<ferrum_types::ApiResponse>,
6072        finish_reason: FinishReason,
6073        execution_attribution: Option<Value>,
6074        lora_metrics: Option<Value>,
6075        pending_stream_drop_notify: Option<Arc<Notify>>,
6076        stream_after_first_gate: Option<Arc<StreamGate>>,
6077        stream_terminal_error: bool,
6078        shutdown_count: AtomicUsize,
6079    }
6080
6081    impl StubLlm {
6082        fn new(text: &str) -> Self {
6083            let mut config = EngineConfig::default();
6084            config.model.model_id = ModelId::new("stub-model");
6085            Self {
6086                config,
6087                resource_authority: ExecutionResourceAuthority::LegacyEngine,
6088                text: text.to_string(),
6089                context_capacity: None,
6090                stream_chunks: None,
6091                stream_final_chunk_separate: false,
6092                stream_tail_without_token: false,
6093                stream_usage: Some(TokenUsage::new(5, 1)),
6094                api_response: None,
6095                finish_reason: FinishReason::EOS,
6096                execution_attribution: None,
6097                lora_metrics: None,
6098                pending_stream_drop_notify: None,
6099                stream_after_first_gate: None,
6100                stream_terminal_error: false,
6101                shutdown_count: AtomicUsize::new(0),
6102            }
6103        }
6104
6105        fn without_stream_usage(text: &str) -> Self {
6106            Self {
6107                stream_usage: None,
6108                ..Self::new(text)
6109            }
6110        }
6111
6112        fn with_stream_chunks(chunks: &[&str]) -> Self {
6113            Self {
6114                text: chunks.concat(),
6115                stream_chunks: Some(chunks.iter().map(|chunk| (*chunk).to_string()).collect()),
6116                stream_usage: Some(TokenUsage::new(5, chunks.len())),
6117                ..Self::new("")
6118            }
6119        }
6120
6121        fn with_separate_final_stream_chunk(chunks: &[&str]) -> Self {
6122            Self {
6123                text: chunks.concat(),
6124                stream_chunks: Some(chunks.iter().map(|chunk| (*chunk).to_string()).collect()),
6125                stream_final_chunk_separate: true,
6126                stream_usage: Some(TokenUsage::new(5, chunks.len())),
6127                ..Self::new("")
6128            }
6129        }
6130
6131        fn with_tokenless_tail(chunks: &[&str]) -> Self {
6132            Self {
6133                stream_tail_without_token: true,
6134                ..Self::with_separate_final_stream_chunk(chunks)
6135            }
6136        }
6137
6138        fn with_api_response(text: &str, api_response: ferrum_types::ApiResponse) -> Self {
6139            Self {
6140                api_response: Some(api_response),
6141                ..Self::new(text)
6142            }
6143        }
6144
6145        fn with_api_response_and_finish_reason(
6146            text: &str,
6147            api_response: ferrum_types::ApiResponse,
6148            finish_reason: FinishReason,
6149        ) -> Self {
6150            Self {
6151                api_response: Some(api_response),
6152                finish_reason,
6153                ..Self::new(text)
6154            }
6155        }
6156
6157        fn with_lora_metrics(text: &str, lora_metrics: Value) -> Self {
6158            Self {
6159                lora_metrics: Some(lora_metrics),
6160                ..Self::new(text)
6161            }
6162        }
6163
6164        fn with_execution_attribution(text: &str, execution_attribution: Value) -> Self {
6165            Self {
6166                execution_attribution: Some(execution_attribution),
6167                ..Self::new(text)
6168            }
6169        }
6170
6171        fn with_pending_stream(drop_notify: Arc<Notify>) -> Self {
6172            Self {
6173                pending_stream_drop_notify: Some(drop_notify),
6174                ..Self::new("")
6175            }
6176        }
6177    }
6178
6179    struct PendingDropStream {
6180        drop_notify: Arc<Notify>,
6181    }
6182
6183    #[derive(Default)]
6184    struct StreamGate {
6185        entered: Notify,
6186        resume: Notify,
6187    }
6188
6189    impl Stream for PendingDropStream {
6190        type Item = ferrum_types::Result<StreamChunk>;
6191
6192        fn poll_next(
6193            self: Pin<&mut Self>,
6194            _cx: &mut std::task::Context<'_>,
6195        ) -> std::task::Poll<Option<Self::Item>> {
6196            std::task::Poll::Pending
6197        }
6198    }
6199
6200    impl Drop for PendingDropStream {
6201        fn drop(&mut self) {
6202            self.drop_notify.notify_one();
6203        }
6204    }
6205
6206    struct StubEmbed {
6207        config: EngineConfig,
6208    }
6209
6210    impl StubEmbed {
6211        fn new() -> Self {
6212            let mut config = EngineConfig::default();
6213            config.model.model_id = ModelId::new("stub-embed");
6214            Self { config }
6215        }
6216    }
6217
6218    struct StubTranscribe {
6219        config: EngineConfig,
6220    }
6221
6222    impl StubTranscribe {
6223        fn new() -> Self {
6224            let mut config = EngineConfig::default();
6225            config.model.model_id = ModelId::new("stub-transcribe");
6226            Self { config }
6227        }
6228    }
6229
6230    struct StubTts {
6231        config: EngineConfig,
6232    }
6233
6234    impl StubTts {
6235        fn new() -> Self {
6236            let mut config = EngineConfig::default();
6237            config.model.model_id = ModelId::new("stub-tts");
6238            Self { config }
6239        }
6240    }
6241
6242    struct FailingLlm {
6243        config: EngineConfig,
6244        fail_after_stream_start: bool,
6245        infer_failure: ferrum_types::FerrumError,
6246        stream_start_failure: ferrum_types::FerrumError,
6247        stream_chunk_failure: ferrum_types::FerrumError,
6248    }
6249
6250    impl FailingLlm {
6251        fn new() -> Self {
6252            let mut config = EngineConfig::default();
6253            config.model.model_id = ModelId::new("failing-model");
6254            Self {
6255                config,
6256                fail_after_stream_start: false,
6257                infer_failure: ferrum_types::FerrumError::internal("stub generation failed"),
6258                stream_start_failure: ferrum_types::FerrumError::internal("stub stream failed"),
6259                stream_chunk_failure: ferrum_types::FerrumError::internal(
6260                    "stub stream chunk failed",
6261                ),
6262            }
6263        }
6264
6265        fn after_stream_start() -> Self {
6266            Self {
6267                fail_after_stream_start: true,
6268                ..Self::new()
6269            }
6270        }
6271
6272        fn resource_exhausted() -> Self {
6273            let failure = ferrum_types::FerrumError::resource_exhausted(
6274                "admission capacity exhausted while reserving request resources",
6275            );
6276            Self {
6277                infer_failure: failure.clone(),
6278                stream_start_failure: failure.clone(),
6279                stream_chunk_failure: failure,
6280                ..Self::new()
6281            }
6282        }
6283
6284        fn context_length_exceeded() -> Self {
6285            let failure = ferrum_types::FerrumError::ContextLengthExceeded {
6286                capacity: 512,
6287                input_tokens: 500,
6288                output_tokens: 100,
6289            };
6290            Self {
6291                infer_failure: failure.clone(),
6292                stream_start_failure: failure,
6293                ..Self::new()
6294            }
6295        }
6296    }
6297
6298    struct CapturingLlm {
6299        config: EngineConfig,
6300        last_request: Mutex<Option<InferenceRequest>>,
6301    }
6302
6303    impl CapturingLlm {
6304        fn new() -> Self {
6305            let mut config = EngineConfig::default();
6306            config.model.model_id = ModelId::new("qwen3");
6307            Self {
6308                config,
6309                last_request: Mutex::new(None),
6310            }
6311        }
6312
6313        fn last_request(&self) -> InferenceRequest {
6314            self.last_request
6315                .lock()
6316                .expect("capture lock")
6317                .clone()
6318                .expect("request captured")
6319        }
6320
6321        fn has_captured_request(&self) -> bool {
6322            self.last_request.lock().expect("capture lock").is_some()
6323        }
6324    }
6325
6326    #[async_trait]
6327    impl InferenceEngine for StubLlm {
6328        async fn status(&self) -> EngineStatus {
6329            EngineStatus {
6330                is_ready: true,
6331                loaded_models: vec![self.config.model.model_id.clone()],
6332                active_requests: 0,
6333                queued_requests: 0,
6334                memory_usage: MemoryUsage {
6335                    total_bytes: 0,
6336                    used_bytes: 0,
6337                    free_bytes: 0,
6338                    gpu_memory_bytes: None,
6339                    cpu_memory_bytes: None,
6340                    cache_memory_bytes: 0,
6341                    utilization_percent: 0.0,
6342                },
6343                uptime_seconds: 0,
6344                last_heartbeat: chrono::Utc::now(),
6345                version: "test".to_string(),
6346            }
6347        }
6348
6349        async fn shutdown(&self) -> ferrum_types::Result<()> {
6350            self.shutdown_count.fetch_add(1, Ordering::AcqRel);
6351            Ok(())
6352        }
6353
6354        fn config(&self) -> &EngineConfig {
6355            &self.config
6356        }
6357
6358        fn metrics(&self) -> EngineMetrics {
6359            EngineMetrics::default()
6360        }
6361
6362        async fn health_check(&self) -> EngineHealthStatus {
6363            EngineHealthStatus::healthy()
6364        }
6365
6366        fn execution_attribution_snapshot(&self) -> Option<Value> {
6367            self.execution_attribution.clone()
6368        }
6369
6370        fn lora_metrics_snapshot(&self) -> Option<Value> {
6371            self.lora_metrics.clone()
6372        }
6373    }
6374
6375    #[async_trait]
6376    impl InferenceEngine for StubEmbed {
6377        async fn status(&self) -> EngineStatus {
6378            EngineStatus {
6379                is_ready: true,
6380                loaded_models: vec![self.config.model.model_id.clone()],
6381                active_requests: 0,
6382                queued_requests: 0,
6383                memory_usage: MemoryUsage {
6384                    total_bytes: 0,
6385                    used_bytes: 0,
6386                    free_bytes: 0,
6387                    gpu_memory_bytes: None,
6388                    cpu_memory_bytes: None,
6389                    cache_memory_bytes: 0,
6390                    utilization_percent: 0.0,
6391                },
6392                uptime_seconds: 0,
6393                last_heartbeat: chrono::Utc::now(),
6394                version: "test".to_string(),
6395            }
6396        }
6397
6398        async fn shutdown(&self) -> ferrum_types::Result<()> {
6399            Ok(())
6400        }
6401
6402        fn config(&self) -> &EngineConfig {
6403            &self.config
6404        }
6405
6406        fn metrics(&self) -> EngineMetrics {
6407            EngineMetrics::default()
6408        }
6409
6410        async fn health_check(&self) -> EngineHealthStatus {
6411            EngineHealthStatus::healthy()
6412        }
6413    }
6414
6415    #[async_trait]
6416    impl EmbedEngine for StubEmbed {
6417        async fn embed_text(&self, text: &str) -> ferrum_types::Result<Vec<f32>> {
6418            Ok(vec![text.len() as f32, 1.0, 0.0])
6419        }
6420
6421        async fn embed_image(&self, image: &str) -> ferrum_types::Result<Vec<f32>> {
6422            Ok(vec![image.len() as f32, 0.0, 1.0])
6423        }
6424
6425        fn embedding_dim(&self) -> usize {
6426            3
6427        }
6428    }
6429
6430    #[async_trait]
6431    impl InferenceEngine for StubTranscribe {
6432        async fn status(&self) -> EngineStatus {
6433            EngineStatus {
6434                is_ready: true,
6435                loaded_models: vec![self.config.model.model_id.clone()],
6436                active_requests: 0,
6437                queued_requests: 0,
6438                memory_usage: MemoryUsage {
6439                    total_bytes: 0,
6440                    used_bytes: 0,
6441                    free_bytes: 0,
6442                    gpu_memory_bytes: None,
6443                    cpu_memory_bytes: None,
6444                    cache_memory_bytes: 0,
6445                    utilization_percent: 0.0,
6446                },
6447                uptime_seconds: 0,
6448                last_heartbeat: chrono::Utc::now(),
6449                version: "test".to_string(),
6450            }
6451        }
6452
6453        async fn shutdown(&self) -> ferrum_types::Result<()> {
6454            Ok(())
6455        }
6456
6457        fn config(&self) -> &EngineConfig {
6458            &self.config
6459        }
6460
6461        fn metrics(&self) -> EngineMetrics {
6462            EngineMetrics::default()
6463        }
6464
6465        async fn health_check(&self) -> EngineHealthStatus {
6466            EngineHealthStatus::healthy()
6467        }
6468    }
6469
6470    #[async_trait]
6471    impl TranscribeEngine for StubTranscribe {
6472        async fn transcribe_file(
6473            &self,
6474            path: &str,
6475            language: Option<&str>,
6476        ) -> ferrum_types::Result<String> {
6477            Ok(format!("file:{path}:{}", language.unwrap_or("auto")))
6478        }
6479
6480        async fn transcribe_bytes(
6481            &self,
6482            data: &[u8],
6483            language: Option<&str>,
6484        ) -> ferrum_types::Result<String> {
6485            Ok(format!(
6486                "bytes:{}:{}",
6487                data.len(),
6488                language.unwrap_or("auto")
6489            ))
6490        }
6491    }
6492
6493    #[async_trait]
6494    impl InferenceEngine for StubTts {
6495        async fn status(&self) -> EngineStatus {
6496            EngineStatus {
6497                is_ready: true,
6498                loaded_models: vec![self.config.model.model_id.clone()],
6499                active_requests: 0,
6500                queued_requests: 0,
6501                memory_usage: MemoryUsage {
6502                    total_bytes: 0,
6503                    used_bytes: 0,
6504                    free_bytes: 0,
6505                    gpu_memory_bytes: None,
6506                    cpu_memory_bytes: None,
6507                    cache_memory_bytes: 0,
6508                    utilization_percent: 0.0,
6509                },
6510                uptime_seconds: 0,
6511                last_heartbeat: chrono::Utc::now(),
6512                version: "test".to_string(),
6513            }
6514        }
6515
6516        async fn shutdown(&self) -> ferrum_types::Result<()> {
6517            Ok(())
6518        }
6519
6520        fn config(&self) -> &EngineConfig {
6521            &self.config
6522        }
6523
6524        fn metrics(&self) -> EngineMetrics {
6525            EngineMetrics::default()
6526        }
6527
6528        async fn health_check(&self) -> EngineHealthStatus {
6529            EngineHealthStatus::healthy()
6530        }
6531    }
6532
6533    #[async_trait]
6534    impl TtsEngine for StubTts {
6535        async fn synthesize_speech(
6536            &self,
6537            _text: &str,
6538            _language: Option<&str>,
6539            _chunk_frames: usize,
6540        ) -> ferrum_types::Result<Vec<Vec<f32>>> {
6541            Ok(vec![vec![0.0, 0.5, -0.5]])
6542        }
6543
6544        fn tts_sample_rate(&self) -> u32 {
6545            16_000
6546        }
6547    }
6548
6549    #[async_trait]
6550    impl InferenceEngine for FailingLlm {
6551        async fn status(&self) -> EngineStatus {
6552            EngineStatus {
6553                is_ready: true,
6554                loaded_models: vec![self.config.model.model_id.clone()],
6555                active_requests: 0,
6556                queued_requests: 0,
6557                memory_usage: MemoryUsage {
6558                    total_bytes: 0,
6559                    used_bytes: 0,
6560                    free_bytes: 0,
6561                    gpu_memory_bytes: None,
6562                    cpu_memory_bytes: None,
6563                    cache_memory_bytes: 0,
6564                    utilization_percent: 0.0,
6565                },
6566                uptime_seconds: 0,
6567                last_heartbeat: chrono::Utc::now(),
6568                version: "test".to_string(),
6569            }
6570        }
6571
6572        async fn shutdown(&self) -> ferrum_types::Result<()> {
6573            Ok(())
6574        }
6575
6576        fn config(&self) -> &EngineConfig {
6577            &self.config
6578        }
6579
6580        fn metrics(&self) -> EngineMetrics {
6581            EngineMetrics::default()
6582        }
6583
6584        async fn health_check(&self) -> EngineHealthStatus {
6585            EngineHealthStatus::healthy()
6586        }
6587    }
6588
6589    #[async_trait]
6590    impl InferenceEngine for CapturingLlm {
6591        async fn status(&self) -> EngineStatus {
6592            EngineStatus {
6593                is_ready: true,
6594                loaded_models: vec![self.config.model.model_id.clone()],
6595                active_requests: 0,
6596                queued_requests: 0,
6597                memory_usage: MemoryUsage {
6598                    total_bytes: 0,
6599                    used_bytes: 0,
6600                    free_bytes: 0,
6601                    gpu_memory_bytes: None,
6602                    cpu_memory_bytes: None,
6603                    cache_memory_bytes: 0,
6604                    utilization_percent: 0.0,
6605                },
6606                uptime_seconds: 0,
6607                last_heartbeat: chrono::Utc::now(),
6608                version: "test".to_string(),
6609            }
6610        }
6611
6612        async fn shutdown(&self) -> ferrum_types::Result<()> {
6613            Ok(())
6614        }
6615
6616        fn config(&self) -> &EngineConfig {
6617            &self.config
6618        }
6619
6620        fn metrics(&self) -> EngineMetrics {
6621            EngineMetrics::default()
6622        }
6623
6624        async fn health_check(&self) -> EngineHealthStatus {
6625            EngineHealthStatus::healthy()
6626        }
6627    }
6628
6629    fn stub_execution_evidence(
6630        request: &InferenceRequest,
6631        output_token_count: usize,
6632    ) -> Option<InferenceExecutionEvidence> {
6633        let requested = &request.evidence_request;
6634        if !requested.capture_prompt_token_ids && !requested.capture_engine_token_timing {
6635            return None;
6636        }
6637        Some(InferenceExecutionEvidence {
6638            prompt_token_ids: requested
6639                .capture_prompt_token_ids
6640                .then(|| vec![TokenId::new(101), TokenId::new(202), TokenId::new(303)])
6641                .unwrap_or_default(),
6642            output_token_ids: (0..output_token_count)
6643                .map(|index| TokenId::new(11 + index as u32))
6644                .collect(),
6645            engine_token_timing: requested.capture_engine_token_timing.then(|| {
6646                EngineTokenTimingEvidence {
6647                    clock_source: "rust_std_instant".to_string(),
6648                    wall_anchor_unix_nanos: 1_700_000_000_000_000_000,
6649                    wall_anchor_max_error_nanos: 500,
6650                    decode_ready_nanos_since_request_start: Some(1_000_000),
6651                    token_commit_nanos_since_request_start: (1..=output_token_count)
6652                        .map(|ordinal| ordinal as u64 * 1_000_000)
6653                        .collect(),
6654                    decode_stage_intervals: Vec::new(),
6655                }
6656            }),
6657        })
6658    }
6659
6660    #[async_trait]
6661    impl LlmInferenceEngine for StubLlm {
6662        fn execution_resource_authority(&self) -> ExecutionResourceAuthority {
6663            self.resource_authority
6664        }
6665
6666        fn context_capacity(&self) -> Option<usize> {
6667            self.context_capacity
6668        }
6669
6670        async fn infer(
6671            &self,
6672            request: InferenceRequest,
6673        ) -> ferrum_types::Result<InferenceResponse> {
6674            let execution_evidence = stub_execution_evidence(&request, 2);
6675            Ok(InferenceResponse {
6676                request_id: request.id,
6677                text: self.text.clone(),
6678                tokens: vec![TokenId::new(11), TokenId::new(12)],
6679                finish_reason: self.finish_reason,
6680                usage: TokenUsage::new(7, 2),
6681                latency_ms: 1,
6682                created_at: chrono::Utc::now(),
6683                metadata: HashMap::new(),
6684                api_response: self.api_response.clone(),
6685                execution_evidence,
6686            })
6687        }
6688
6689        async fn infer_stream(
6690            &self,
6691            request: InferenceRequest,
6692        ) -> ferrum_types::Result<
6693            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6694        > {
6695            if let Some(drop_notify) = self.pending_stream_drop_notify.as_ref() {
6696                return Ok(Box::pin(PendingDropStream {
6697                    drop_notify: Arc::clone(drop_notify),
6698                }));
6699            }
6700            if let Some(chunks) = &self.stream_chunks {
6701                let completion_token_count = self
6702                    .stream_usage
6703                    .as_ref()
6704                    .map(|usage| usage.completion_tokens)
6705                    .unwrap_or(chunks.len());
6706                let execution_evidence = stub_execution_evidence(&request, completion_token_count);
6707                let request_id = request.id;
6708                let mut stream_chunks = Vec::with_capacity(
6709                    chunks.len() + usize::from(self.stream_final_chunk_separate),
6710                );
6711                let last = chunks.len().saturating_sub(1);
6712                for (index, text) in chunks.iter().enumerate() {
6713                    let is_final_text_chunk = index == last && !self.stream_final_chunk_separate;
6714                    stream_chunks.push(Ok(StreamChunk {
6715                        request_id: request_id.clone(),
6716                        text: text.clone(),
6717                        token: (!(self.stream_tail_without_token && index == last))
6718                            .then_some(TokenId::new(11 + index as u32)),
6719                        finish_reason: is_final_text_chunk.then_some(self.finish_reason),
6720                        usage: is_final_text_chunk
6721                            .then(|| self.stream_usage.clone())
6722                            .flatten(),
6723                        created_at: chrono::Utc::now(),
6724                        metadata: HashMap::new(),
6725                        api_response: is_final_text_chunk
6726                            .then(|| self.api_response.clone())
6727                            .flatten(),
6728                        execution_evidence: is_final_text_chunk
6729                            .then(|| execution_evidence.clone())
6730                            .flatten(),
6731                    }));
6732                }
6733                if self.stream_final_chunk_separate {
6734                    stream_chunks.push(Ok(StreamChunk {
6735                        request_id,
6736                        text: String::new(),
6737                        token: None,
6738                        finish_reason: Some(self.finish_reason),
6739                        usage: self.stream_usage.clone(),
6740                        created_at: chrono::Utc::now(),
6741                        metadata: HashMap::new(),
6742                        api_response: self.api_response.clone(),
6743                        execution_evidence,
6744                    }));
6745                }
6746                if self.stream_terminal_error {
6747                    *stream_chunks.last_mut().expect("nonempty fixture stream") = Err(
6748                        ferrum_types::FerrumError::internal("fixture generation failed"),
6749                    );
6750                }
6751                if let Some(gate) = self.stream_after_first_gate.clone() {
6752                    return Ok(Box::pin(stream::unfold(
6753                        (stream_chunks.into_iter().enumerate(), gate),
6754                        |(mut chunks, gate)| async move {
6755                            let (index, chunk) = chunks.next()?;
6756                            if index == 1 {
6757                                gate.entered.notify_one();
6758                                gate.resume.notified().await;
6759                            }
6760                            Some((chunk, (chunks, gate)))
6761                        },
6762                    )));
6763                }
6764                return Ok(Box::pin(stream::iter(stream_chunks)));
6765            }
6766
6767            let execution_evidence = stub_execution_evidence(&request, 1);
6768            let chunk = StreamChunk {
6769                request_id: request.id,
6770                text: self.text.clone(),
6771                token: Some(TokenId::new(11)),
6772                finish_reason: Some(self.finish_reason),
6773                usage: self.stream_usage.clone(),
6774                created_at: chrono::Utc::now(),
6775                metadata: HashMap::new(),
6776                api_response: self.api_response.clone(),
6777                execution_evidence,
6778            };
6779            Ok(Box::pin(stream::iter(vec![Ok(chunk)])))
6780        }
6781    }
6782
6783    #[async_trait]
6784    impl LlmInferenceEngine for FailingLlm {
6785        async fn infer(
6786            &self,
6787            _request: InferenceRequest,
6788        ) -> ferrum_types::Result<InferenceResponse> {
6789            Err(self.infer_failure.clone())
6790        }
6791
6792        async fn infer_stream(
6793            &self,
6794            request: InferenceRequest,
6795        ) -> ferrum_types::Result<
6796            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6797        > {
6798            if self.fail_after_stream_start {
6799                let _request_id = request.id;
6800                return Ok(Box::pin(stream::iter(vec![Err(self
6801                    .stream_chunk_failure
6802                    .clone())])));
6803            }
6804            Err(self.stream_start_failure.clone())
6805        }
6806    }
6807
6808    #[async_trait]
6809    impl LlmInferenceEngine for CapturingLlm {
6810        async fn infer(
6811            &self,
6812            request: InferenceRequest,
6813        ) -> ferrum_types::Result<InferenceResponse> {
6814            *self.last_request.lock().expect("capture lock") = Some(request.clone());
6815            Ok(InferenceResponse {
6816                request_id: request.id,
6817                text: "captured".to_string(),
6818                tokens: vec![TokenId::new(21)],
6819                finish_reason: FinishReason::Stop,
6820                usage: TokenUsage::new(9, 1),
6821                latency_ms: 1,
6822                created_at: chrono::Utc::now(),
6823                metadata: HashMap::new(),
6824                api_response: None,
6825                execution_evidence: None,
6826            })
6827        }
6828
6829        async fn infer_stream(
6830            &self,
6831            request: InferenceRequest,
6832        ) -> ferrum_types::Result<
6833            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6834        > {
6835            *self.last_request.lock().expect("capture lock") = Some(request.clone());
6836            let chunk = StreamChunk {
6837                request_id: request.id,
6838                text: "captured".to_string(),
6839                token: Some(TokenId::new(21)),
6840                finish_reason: Some(FinishReason::Stop),
6841                usage: Some(TokenUsage::new(9, 1)),
6842                created_at: chrono::Utc::now(),
6843                metadata: HashMap::new(),
6844                api_response: None,
6845                execution_evidence: None,
6846            };
6847            Ok(Box::pin(stream::iter(vec![Ok(chunk)])))
6848        }
6849    }
6850
6851    fn state_with_stub(text: &str) -> AppState {
6852        AppState::default().with_llm(Arc::new(StubLlm::new(text)))
6853    }
6854
6855    fn router_with_stub(text: &str) -> Router {
6856        AxumServer::from_llm(Arc::new(StubLlm::new(text))).build_router()
6857    }
6858
6859    fn router_with_stub_and_template(text: &str, template: ModelChatTemplate) -> Router {
6860        AxumServer::from_llm(Arc::new(StubLlm::new(text)))
6861            .with_prompt_template(Some(template))
6862            .build_router()
6863    }
6864
6865    fn router_with_stub_and_request_dump_dir(text: &str, request_dump_dir: PathBuf) -> Router {
6866        AxumServer::from_state(
6867            AppState::default()
6868                .with_llm(Arc::new(StubLlm::new(text)))
6869                .with_request_dump_dir(Some(request_dump_dir)),
6870        )
6871        .build_router()
6872    }
6873
6874    fn router_with_stub_request_dump_and_profile(
6875        text: &str,
6876        request_dump_dir: PathBuf,
6877        profile_jsonl: PathBuf,
6878    ) -> Router {
6879        AxumServer::from_state(
6880            AppState::default()
6881                .with_llm(Arc::new(StubLlm::new(text)))
6882                .with_request_dump_dir(Some(request_dump_dir))
6883                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
6884                .with_profile_jsonl(Some(profile_jsonl)),
6885        )
6886        .build_router()
6887    }
6888
6889    fn router_with_stub_stream_chunks(chunks: &[&str]) -> Router {
6890        AxumServer::from_llm(Arc::new(StubLlm::with_stream_chunks(chunks))).build_router()
6891    }
6892
6893    fn router_with_stub_finish_reason(text: &str, finish_reason: FinishReason) -> Router {
6894        AxumServer::from_llm(Arc::new(StubLlm {
6895            finish_reason,
6896            ..StubLlm::new(text)
6897        }))
6898        .build_router()
6899    }
6900
6901    fn router_with_stub_stream_chunks_and_request_dump_dir(
6902        chunks: &[&str],
6903        request_dump_dir: PathBuf,
6904    ) -> Router {
6905        AxumServer::from_state(
6906            AppState::default()
6907                .with_llm(Arc::new(StubLlm::with_stream_chunks(chunks)))
6908                .with_request_dump_dir(Some(request_dump_dir)),
6909        )
6910        .build_router()
6911    }
6912
6913    fn router_with_stub_stream_request_dump_and_profile(
6914        chunks: &[&str],
6915        request_dump_dir: PathBuf,
6916        profile_jsonl: PathBuf,
6917    ) -> Router {
6918        AxumServer::from_state(
6919            AppState::default()
6920                .with_llm(Arc::new(StubLlm::with_stream_chunks(chunks)))
6921                .with_request_dump_dir(Some(request_dump_dir))
6922                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
6923                .with_profile_jsonl(Some(profile_jsonl)),
6924        )
6925        .build_router()
6926    }
6927
6928    fn router_with_stub_separate_final_stream_chunk(chunks: &[&str]) -> Router {
6929        AxumServer::from_llm(Arc::new(StubLlm::with_separate_final_stream_chunk(chunks)))
6930            .build_router()
6931    }
6932
6933    fn router_with_stub_api_response(
6934        text: &str,
6935        api_response: ferrum_types::ApiResponse,
6936    ) -> Router {
6937        AxumServer::from_llm(Arc::new(StubLlm::with_api_response(text, api_response)))
6938            .build_router()
6939    }
6940
6941    fn router_with_stub_api_response_and_finish_reason(
6942        text: &str,
6943        api_response: ferrum_types::ApiResponse,
6944        finish_reason: FinishReason,
6945    ) -> Router {
6946        AxumServer::from_llm(Arc::new(StubLlm::with_api_response_and_finish_reason(
6947            text,
6948            api_response,
6949            finish_reason,
6950        )))
6951        .build_router()
6952    }
6953
6954    fn weather_tool_api_response() -> ferrum_types::ApiResponse {
6955        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
6956            message: ferrum_types::ApiChatMessage {
6957                role: ferrum_types::ApiMessageRole::Assistant,
6958                content: String::new(),
6959                name: None,
6960                tool_calls: vec![ferrum_types::ApiToolCall {
6961                    id: "call_1".to_string(),
6962                    tool_type: "function".to_string(),
6963                    function: ferrum_types::ApiFunctionCall {
6964                        name: "weather".to_string(),
6965                        arguments: "{\"city\":\"Paris\"}".to_string(),
6966                    },
6967                }],
6968                tool_call_id: None,
6969                function_call: None,
6970            },
6971            finish_reason: Some("tool_calls".to_string()),
6972        })
6973    }
6974
6975    fn weather_tool_api_response_with_commentary() -> ferrum_types::ApiResponse {
6976        let mut response = weather_tool_api_response();
6977        let ferrum_types::ApiResponse::Chat(chat) = &mut response else {
6978            unreachable!("weather response is chat")
6979        };
6980        chat.message.content = "I will check.".to_string();
6981        response
6982    }
6983
6984    fn namespaced_tool_api_response() -> ferrum_types::ApiResponse {
6985        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
6986            message: ferrum_types::ApiChatMessage {
6987                role: ferrum_types::ApiMessageRole::Assistant,
6988                content: String::new(),
6989                name: None,
6990                tool_calls: vec![ferrum_types::ApiToolCall {
6991                    id: "call_ns_1".to_string(),
6992                    tool_type: "function".to_string(),
6993                    function: ferrum_types::ApiFunctionCall {
6994                        name: "collaboration__wait_agent".to_string(),
6995                        arguments: "{}".to_string(),
6996                    },
6997                }],
6998                tool_call_id: None,
6999                function_call: None,
7000            },
7001            finish_reason: Some("tool_calls".to_string()),
7002        })
7003    }
7004
7005    fn two_tool_api_response() -> ferrum_types::ApiResponse {
7006        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
7007            message: ferrum_types::ApiChatMessage {
7008                role: ferrum_types::ApiMessageRole::Assistant,
7009                content: String::new(),
7010                name: None,
7011                tool_calls: vec![
7012                    ferrum_types::ApiToolCall {
7013                        id: "call_1".to_string(),
7014                        tool_type: "function".to_string(),
7015                        function: ferrum_types::ApiFunctionCall {
7016                            name: "weather".to_string(),
7017                            arguments: "{}".to_string(),
7018                        },
7019                    },
7020                    ferrum_types::ApiToolCall {
7021                        id: "call_2".to_string(),
7022                        tool_type: "function".to_string(),
7023                        function: ferrum_types::ApiFunctionCall {
7024                            name: "clock".to_string(),
7025                            arguments: "{}".to_string(),
7026                        },
7027                    },
7028                ],
7029                tool_call_id: None,
7030                function_call: None,
7031            },
7032            finish_reason: Some("tool_calls".to_string()),
7033        })
7034    }
7035
7036    fn router_with_stub_without_stream_usage(text: &str) -> Router {
7037        AxumServer::from_llm(Arc::new(StubLlm::without_stream_usage(text))).build_router()
7038    }
7039
7040    fn router_without_llm() -> Router {
7041        AxumServer::from_state(AppState::default()).build_router()
7042    }
7043
7044    fn router_with_failing_llm() -> Router {
7045        AxumServer::from_llm(Arc::new(FailingLlm::new())).build_router()
7046    }
7047
7048    fn router_with_failing_llm_and_request_dump_dir(request_dump_dir: PathBuf) -> Router {
7049        AxumServer::from_state(
7050            AppState::default()
7051                .with_llm(Arc::new(FailingLlm::new()))
7052                .with_request_dump_dir(Some(request_dump_dir)),
7053        )
7054        .build_router()
7055    }
7056
7057    fn router_with_failing_llm_request_dump_and_profile(
7058        request_dump_dir: PathBuf,
7059        profile_jsonl: PathBuf,
7060    ) -> Router {
7061        AxumServer::from_state(
7062            AppState::default()
7063                .with_llm(Arc::new(FailingLlm::new()))
7064                .with_request_dump_dir(Some(request_dump_dir))
7065                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
7066                .with_profile_jsonl(Some(profile_jsonl)),
7067        )
7068        .build_router()
7069    }
7070
7071    fn router_with_resource_exhausted_llm_and_request_dump_dir(
7072        request_dump_dir: PathBuf,
7073    ) -> Router {
7074        AxumServer::from_state(
7075            AppState::default()
7076                .with_llm(Arc::new(FailingLlm::resource_exhausted()))
7077                .with_request_dump_dir(Some(request_dump_dir)),
7078        )
7079        .build_router()
7080    }
7081
7082    fn router_with_stream_chunk_failing_llm() -> Router {
7083        AxumServer::from_llm(Arc::new(FailingLlm::after_stream_start())).build_router()
7084    }
7085
7086    fn router_with_stream_chunk_failing_llm_and_request_dump_dir(
7087        request_dump_dir: PathBuf,
7088    ) -> Router {
7089        AxumServer::from_state(
7090            AppState::default()
7091                .with_llm(Arc::new(FailingLlm::after_stream_start()))
7092                .with_request_dump_dir(Some(request_dump_dir)),
7093        )
7094        .build_router()
7095    }
7096
7097    fn router_with_capturing_llm() -> (Router, Arc<CapturingLlm>) {
7098        let engine = Arc::new(CapturingLlm::new());
7099        let registry = ServedModelRegistry::try_new(
7100            "qwen3",
7101            ServedModelKind::Llm,
7102            vec![
7103                "qwen3".to_string(),
7104                "stub-model".to_string(),
7105                "served-alias".to_string(),
7106            ],
7107            vec![],
7108        )
7109        .unwrap();
7110        let router = AxumServer::from_llm(engine.clone())
7111            .with_served_model_registry(registry)
7112            .build_router();
7113        (router, engine)
7114    }
7115
7116    fn unique_request_dump_dir(test_name: &str) -> PathBuf {
7117        let path =
7118            std::env::temp_dir().join(format!("ferrum-server-{test_name}-{}", Uuid::new_v4()));
7119        fs::create_dir_all(&path).expect("create request dump dir");
7120        path
7121    }
7122
7123    fn unique_profile_jsonl(test_name: &str) -> PathBuf {
7124        std::env::temp_dir().join(format!(
7125            "ferrum-server-{test_name}-{}.jsonl",
7126            Uuid::new_v4()
7127        ))
7128    }
7129
7130    fn only_replay_bundle(root: &Path) -> PathBuf {
7131        let mut dirs = fs::read_dir(root)
7132            .expect("read request dump dir")
7133            .filter_map(|entry| {
7134                let path = entry.expect("dir entry").path();
7135                path.is_dir().then_some(path)
7136            })
7137            .collect::<Vec<_>>();
7138        dirs.sort();
7139        assert_eq!(
7140            dirs.len(),
7141            1,
7142            "expected exactly one replay bundle in {root:?}"
7143        );
7144        dirs.remove(0)
7145    }
7146
7147    fn read_json_file(path: impl AsRef<Path>) -> Value {
7148        let path = path.as_ref();
7149        let text = fs::read_to_string(path).unwrap_or_else(|err| {
7150            panic!("failed to read {}: {}", path.display(), err);
7151        });
7152        serde_json::from_str(&text).unwrap_or_else(|err| {
7153            panic!("failed to parse {}: {}", path.display(), err);
7154        })
7155    }
7156
7157    fn read_profile_events(path: &Path) -> Vec<Value> {
7158        let text = fs::read_to_string(path)
7159            .unwrap_or_else(|err| panic!("failed to read {}: {}", path.display(), err));
7160        text.lines()
7161            .filter(|line| !line.trim().is_empty())
7162            .map(|line| serde_json::from_str::<Value>(line).expect("profile event json"))
7163            .collect()
7164    }
7165
7166    fn assert_chat_failure_replay_bundle(
7167        root: &Path,
7168        expected_phase: &str,
7169        expected_error_kind: &str,
7170        expected_message: &str,
7171    ) {
7172        let bundle = only_replay_bundle(root);
7173        let request = read_json_file(bundle.join("request.json"));
7174        let request_id = request["request_id"]
7175            .as_str()
7176            .expect("request id")
7177            .to_string();
7178        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
7179        assert_eq!(bad_scan["request_id"], request_id);
7180        assert_eq!(bad_scan["failure_kind"], "error");
7181        assert_eq!(bad_scan["failure_phase"], expected_phase);
7182        assert_eq!(bad_scan["error_kind"], expected_error_kind);
7183
7184        let diagnostics = read_json_file(bundle.join("failure_diagnostics.json"));
7185        assert_eq!(diagnostics["request_id"], request_id);
7186        assert_eq!(diagnostics["failure_kind"], "error");
7187        assert_eq!(diagnostics["first_failure_event"]["phase"], expected_phase);
7188        assert_eq!(
7189            diagnostics["first_failure_event"]["error_kind"],
7190            expected_error_kind
7191        );
7192        assert_eq!(diagnostics["nearest_request_id"], request_id);
7193        assert!(diagnostics["log_excerpt"]
7194            .as_str()
7195            .expect("log excerpt")
7196            .contains(expected_message));
7197        assert!(bundle.join("replay.command.json").is_file());
7198    }
7199
7200    fn assert_chat_success_replay_bundle(
7201        root: &Path,
7202        expected_token_ids: &[u32],
7203        expected_finish_reason: &str,
7204        expected_output_text: &str,
7205    ) {
7206        let bundle = only_replay_bundle(root);
7207        let request = read_json_file(bundle.join("request.json"));
7208        let request_id = request["request_id"]
7209            .as_str()
7210            .expect("request id")
7211            .to_string();
7212        let prompt_tokens = read_json_file(bundle.join("prompt_token_ids.json"));
7213        assert_eq!(prompt_tokens["request_id"], request_id);
7214        assert_eq!(prompt_tokens["token_ids"], json!([101, 202, 303]));
7215        assert_eq!(prompt_tokens["token_count"], 3);
7216        assert!(prompt_tokens["unavailable_reason"].is_null());
7217        let output_tokens = read_json_file(bundle.join("output_token_ids.json"));
7218        assert_eq!(output_tokens["request_id"], request_id);
7219        assert_eq!(output_tokens["token_ids"], json!(expected_token_ids));
7220        assert_eq!(output_tokens["token_count"], expected_token_ids.len());
7221        assert_eq!(output_tokens["finish_reason"], expected_finish_reason);
7222        assert!(output_tokens["unavailable_reason"].is_null());
7223
7224        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
7225        assert_eq!(bad_scan["request_id"], request_id);
7226        assert_eq!(bad_scan["bad_output"], false);
7227        assert_eq!(bad_scan["failure_kind"], serde_json::Value::Null);
7228        assert_eq!(
7229            bad_scan["output_chars"],
7230            expected_output_text.chars().count()
7231        );
7232        assert_eq!(
7233            bad_scan["classified_output_sha256"],
7234            sha256_hex(expected_output_text.as_bytes())
7235        );
7236
7237        let output_text_bytes = fs::read(bundle.join("output_text.txt")).unwrap();
7238        assert_eq!(bad_scan["output_sha256"], sha256_hex(&output_text_bytes));
7239        let output_text = String::from_utf8(output_text_bytes).unwrap();
7240        assert!(output_text.contains("[redacted actual output]"));
7241        assert!(output_text.contains(&format!(
7242            "sha256={}",
7243            sha256_hex(expected_output_text.as_bytes())
7244        )));
7245        assert!(output_text.contains(&format!("chars={}", expected_output_text.chars().count())));
7246
7247        let replay_body = read_json_file(bundle.join("replay_body.json"));
7248        assert_eq!(replay_body["messages"][0]["role"], "user");
7249        assert_eq!(replay_body["messages"][0]["content"], "[redacted]");
7250        assert_eq!(replay_body["messages"][0]["content_redacted"], true);
7251
7252        let replay = read_json_file(bundle.join("replay.command.json"));
7253        assert_eq!(replay["requires_running_server"], true);
7254        let argv = replay["argv"].as_array().expect("replay argv");
7255        assert!(argv.iter().any(|item| item == "--data-binary"));
7256        assert!(argv.iter().any(|item| {
7257            item.as_str()
7258                .is_some_and(|value| value.starts_with('@') && value.ends_with("replay_body.json"))
7259        }));
7260        assert_eq!(replay["engine_replay"]["requires_http_server"], false);
7261        let engine_argv = replay["engine_replay"]["argv"]
7262            .as_array()
7263            .expect("engine replay argv");
7264        assert!(engine_argv.iter().any(|item| item == "replay-bundle"));
7265    }
7266
7267    fn router_with_capturing_llm_and_template(
7268        template: ModelChatTemplate,
7269    ) -> (Router, Arc<CapturingLlm>) {
7270        router_with_capturing_llm_and_template_default(template, None)
7271    }
7272
7273    fn router_with_capturing_llm_and_template_default(
7274        template: ModelChatTemplate,
7275        default_enable_thinking: Option<bool>,
7276    ) -> (Router, Arc<CapturingLlm>) {
7277        let engine = Arc::new(CapturingLlm::new());
7278        let registry = ServedModelRegistry::try_new(
7279            "qwen3",
7280            ServedModelKind::Llm,
7281            vec!["served-alias".to_string()],
7282            vec![],
7283        )
7284        .unwrap();
7285        let router = AxumServer::from_llm(engine.clone())
7286            .with_served_model_registry(registry)
7287            .with_prompt_template(Some(template))
7288            .with_default_enable_thinking(default_enable_thinking)
7289            .build_router();
7290        (router, engine)
7291    }
7292
7293    fn qwen36_chat_template() -> ModelChatTemplate {
7294        ModelChatTemplate::new(
7295            include_str!("../tests/fixtures/chat_template/Qwen__Qwen3.6-35B-A3B/template.jinja"),
7296            "Qwen/Qwen3.6-35B-A3B",
7297        )
7298    }
7299
7300    async fn capture_qwen36_tool_history_request(
7301        reasoning_fields: Value,
7302        stream: bool,
7303    ) -> InferenceRequest {
7304        let mut assistant = json!({
7305            "role": "assistant",
7306            "content": null,
7307            "tool_calls": [{
7308                "id": "call_1",
7309                "type": "function",
7310                "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
7311            }]
7312        });
7313        assistant
7314            .as_object_mut()
7315            .expect("assistant message object")
7316            .extend(
7317                reasoning_fields
7318                    .as_object()
7319                    .expect("reasoning fields object")
7320                    .clone(),
7321            );
7322        let (router, engine) = router_with_capturing_llm_and_template(qwen36_chat_template());
7323        let response = post_json(
7324            router,
7325            "/v1/chat/completions",
7326            json!({
7327                "model": "served-alias",
7328                "messages": [
7329                    {"role": "user", "content": "Use the weather tool."},
7330                    assistant,
7331                    {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
7332                ],
7333                "tools": [{
7334                    "type": "function",
7335                    "function": {
7336                        "name": "weather",
7337                        "description": "Get weather",
7338                        "parameters": {
7339                            "type": "object",
7340                            "properties": {"city": {"type": "string"}},
7341                            "required": ["city"]
7342                        }
7343                    }
7344                }],
7345                "stream": stream
7346            }),
7347        )
7348        .await;
7349        assert_eq!(response.status(), AxumStatusCode::OK);
7350        if stream {
7351            assert!(response_text(response).await.contains("[DONE]"));
7352        }
7353        engine.last_request()
7354    }
7355
7356    fn router_with_capturing_lora_llm() -> (Router, Arc<CapturingLlm>) {
7357        let engine = Arc::new(CapturingLlm::new());
7358        let router = AxumServer::from_llm(engine.clone())
7359            .with_lora_adapters(
7360                "qwen3",
7361                vec![LoraAdapterModel::new(
7362                    "sql",
7363                    "qwen3:sql",
7364                    "/tmp/sql-adapter",
7365                )],
7366            )
7367            .unwrap()
7368            .build_router();
7369        (router, engine)
7370    }
7371
7372    fn router_with_stub_embed() -> Router {
7373        AxumServer::from_embed(Arc::new(StubEmbed::new())).build_router()
7374    }
7375
7376    fn router_with_stub_transcribe() -> Router {
7377        AxumServer::from_transcribe(Arc::new(StubTranscribe::new())).build_router()
7378    }
7379
7380    fn router_with_stub_tts() -> Router {
7381        AxumServer::from_tts(Arc::new(StubTts::new())).build_router()
7382    }
7383
7384    async fn post_json(app: Router, path: &str, body: Value) -> Response {
7385        app.oneshot(
7386            Request::builder()
7387                .method("POST")
7388                .uri(path)
7389                .header(header::CONTENT_TYPE, "application/json")
7390                .body(Body::from(body.to_string()))
7391                .expect("request"),
7392        )
7393        .await
7394        .expect("route response")
7395    }
7396
7397    async fn post_json_with_benchmark_correlation(
7398        app: Router,
7399        path: &str,
7400        body: Value,
7401        correlation: &BenchmarkRequestCorrelation,
7402    ) -> Response {
7403        app.oneshot(
7404            Request::builder()
7405                .method("POST")
7406                .uri(path)
7407                .header(header::CONTENT_TYPE, "application/json")
7408                .header(BENCHMARK_RUN_ID_HEADER, &correlation.benchmark_run_id)
7409                .header(BENCHMARK_CELL_ID_HEADER, &correlation.cell_id)
7410                .header(
7411                    BENCHMARK_REPEAT_INDEX_HEADER,
7412                    correlation.repeat_index.to_string(),
7413                )
7414                .header(BENCHMARK_PHASE_HEADER, correlation.phase.as_str())
7415                .header(
7416                    BENCHMARK_REQUEST_INDEX_HEADER,
7417                    correlation.request_index.to_string(),
7418                )
7419                .body(Body::from(body.to_string()))
7420                .expect("request"),
7421        )
7422        .await
7423        .expect("route response")
7424    }
7425
7426    async fn post_raw_json(app: Router, path: &str, body: &str) -> Response {
7427        app.oneshot(
7428            Request::builder()
7429                .method("POST")
7430                .uri(path)
7431                .header(header::CONTENT_TYPE, "application/json")
7432                .body(Body::from(body.to_string()))
7433                .expect("request"),
7434        )
7435        .await
7436        .expect("route response")
7437    }
7438
7439    async fn post_multipart(app: Router, path: &str, boundary: &str, body: &str) -> Response {
7440        app.oneshot(
7441            Request::builder()
7442                .method("POST")
7443                .uri(path)
7444                .header(
7445                    header::CONTENT_TYPE,
7446                    format!("multipart/form-data; boundary={boundary}"),
7447                )
7448                .body(Body::from(body.to_string()))
7449                .expect("request"),
7450        )
7451        .await
7452        .expect("route response")
7453    }
7454
7455    async fn get(app: Router, path: &str) -> Response {
7456        app.oneshot(
7457            Request::builder()
7458                .method("GET")
7459                .uri(path)
7460                .body(Body::empty())
7461                .expect("request"),
7462        )
7463        .await
7464        .expect("route response")
7465    }
7466
7467    async fn response_json(response: Response) -> Value {
7468        let bytes = to_bytes(response.into_body(), usize::MAX)
7469            .await
7470            .expect("body bytes");
7471        serde_json::from_slice(&bytes).expect("json body")
7472    }
7473
7474    async fn response_text(response: Response) -> String {
7475        let bytes = to_bytes(response.into_body(), usize::MAX)
7476            .await
7477            .expect("body bytes");
7478        String::from_utf8(bytes.to_vec()).expect("utf8 body")
7479    }
7480
7481    fn responses_sse_json_events(body: &str) -> Vec<Value> {
7482        body.lines()
7483            .filter_map(|line| line.strip_prefix("data: "))
7484            .filter(|data| *data != "[DONE]")
7485            .map(|data| serde_json::from_str(data).expect("Responses SSE JSON event"))
7486            .collect()
7487    }
7488
7489    async fn response_bytes(response: Response) -> Vec<u8> {
7490        to_bytes(response.into_body(), usize::MAX)
7491            .await
7492            .expect("body bytes")
7493            .to_vec()
7494    }
7495
7496    async fn error_json(error: ServerError) -> (AxumStatusCode, Value) {
7497        let response = error.into_response();
7498        let status = response.status();
7499        (status, response_json(response).await)
7500    }
7501
7502    fn assert_openai_stream_error(body: &str, expected_message: &str) {
7503        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
7504        assert!(
7505            body.contains("\"error\":{\"message\":\""),
7506            "stream failure should emit OpenAI error envelope: {body}"
7507        );
7508        assert!(
7509            body.contains(expected_message),
7510            "stream failure should include engine error message {expected_message:?}: {body}"
7511        );
7512        assert!(
7513            body.contains("\"type\":\"internal_server_error\""),
7514            "stream failure should use internal_server_error: {body}"
7515        );
7516        assert!(
7517            !body.contains("{\"error\":\""),
7518            "stream failure must not use legacy bare error payload: {body}"
7519        );
7520    }
7521
7522    fn chat_request(extra: Value) -> ChatCompletionsRequest {
7523        let mut value = json!({
7524            "model": "stub-model",
7525            "messages": [{"role": "user", "content": "hello"}],
7526            "max_tokens": 8
7527        });
7528        let obj = value.as_object_mut().unwrap();
7529        for (k, v) in extra.as_object().unwrap() {
7530            obj.insert(k.clone(), v.clone());
7531        }
7532        serde_json::from_value(value).expect("chat request")
7533    }
7534
7535    #[tokio::test]
7536    async fn responses_route_returns_sync_text_and_usage() {
7537        let response = post_json(
7538            router_with_stub("hello from ferrum"),
7539            "/v1/responses",
7540            json!({
7541                "model": "stub-model",
7542                "input": "hello",
7543                "store": false
7544            }),
7545        )
7546        .await;
7547        assert_eq!(response.status(), AxumStatusCode::OK);
7548        let body = response_json(response).await;
7549        assert_eq!(body["object"], "response");
7550        assert_eq!(body["status"], "completed");
7551        assert_eq!(body["store"], false);
7552        assert_eq!(body["output"][0]["type"], "message");
7553        assert_eq!(body["output"][0]["phase"], "final_answer");
7554        assert_eq!(body["output"][0]["content"][0]["text"], "hello from ferrum");
7555        assert_eq!(body["usage"]["input_tokens"], 7);
7556        assert_eq!(body["usage"]["output_tokens"], 2);
7557        assert_eq!(body["usage"]["total_tokens"], 9);
7558        assert_eq!(body["presence_penalty"], 0.0);
7559        assert_eq!(body["frequency_penalty"], 0.0);
7560    }
7561
7562    #[tokio::test]
7563    async fn responses_route_streams_ordered_text_events_once() {
7564        let response = post_json(
7565            router_with_stub_stream_chunks(&["he", "llo"]),
7566            "/v1/responses",
7567            json!({
7568                "model": "stub-model",
7569                "input": [{"role": "user", "content": "say hello"}],
7570                "stream": true
7571            }),
7572        )
7573        .await;
7574        assert_eq!(response.status(), AxumStatusCode::OK);
7575        let body = response_text(response).await;
7576        for event in [
7577            "response.created",
7578            "response.output_item.added",
7579            "response.output_text.delta",
7580            "response.output_text.done",
7581            "response.output_item.done",
7582            "response.completed",
7583        ] {
7584            assert!(
7585                body.contains(&format!("event: {event}")),
7586                "missing {event}: {body}"
7587            );
7588        }
7589        assert_eq!(
7590            body.matches("event: response.completed").count(),
7591            1,
7592            "completed must be emitted exactly once: {body}"
7593        );
7594        assert!(
7595            body.contains("\"delta\":\"he\""),
7596            "missing first delta: {body}"
7597        );
7598        assert!(
7599            body.contains("\"delta\":\"llo\""),
7600            "missing second delta: {body}"
7601        );
7602        assert!(body.contains("\"input_tokens\":5"), "missing usage: {body}");
7603        let events = responses_sse_json_events(&body);
7604        let message_added = events
7605            .iter()
7606            .find(|event| {
7607                event["type"] == "response.output_item.added" && event["item"]["type"] == "message"
7608            })
7609            .expect("message item added");
7610        assert!(
7611            message_added["item"].get("phase").is_none(),
7612            "stream must not guess phase before later tool calls are known: {body}"
7613        );
7614        let message_done = events
7615            .iter()
7616            .find(|event| {
7617                event["type"] == "response.output_item.done" && event["item"]["type"] == "message"
7618            })
7619            .expect("message item done");
7620        assert_eq!(message_done["item"]["phase"], "final_answer");
7621        let terminal = events
7622            .iter()
7623            .find(|event| event["type"] == "response.completed")
7624            .expect("completed response");
7625        assert_eq!(terminal["response"]["output"][0]["phase"], "final_answer");
7626        let completed = body
7627            .find("event: response.completed")
7628            .expect("completed event");
7629        let done = body.find("data: [DONE]").expect("terminal DONE marker");
7630        assert!(
7631            completed < done,
7632            "DONE must follow response.completed: {body}"
7633        );
7634    }
7635
7636    #[tokio::test]
7637    async fn responses_route_supports_stateless_function_round_trip() {
7638        let tool = json!({
7639            "type": "function",
7640            "name": "weather",
7641            "description": "Get weather",
7642            "parameters": {
7643                "type": "object",
7644                "properties": {"city": {"type": "string"}},
7645                "required": ["city"]
7646            }
7647        });
7648        let first = post_json(
7649            router_with_stub_api_response("", weather_tool_api_response()),
7650            "/v1/responses",
7651            json!({
7652                "model": "stub-model",
7653                "input": "Use the weather tool",
7654                "tools": [tool.clone()],
7655                "tool_choice": "auto"
7656            }),
7657        )
7658        .await;
7659        assert_eq!(first.status(), AxumStatusCode::OK);
7660        let first_body = response_json(first).await;
7661        let call = first_body["output"][0].clone();
7662        assert_eq!(call["type"], "function_call");
7663        assert_eq!(call["call_id"], "call_1");
7664        assert_eq!(call["name"], "weather");
7665        assert_eq!(call["arguments"], "{\"city\":\"Paris\"}");
7666
7667        let second = post_json(
7668            router_with_stub("weather received"),
7669            "/v1/responses",
7670            json!({
7671                "model": "stub-model",
7672                "input": [
7673                    {"role": "user", "content": "Use the weather tool"},
7674                    call,
7675                    {"type": "function_call_output", "call_id": "call_1", "output": "sunny"}
7676                ],
7677                "tools": [tool]
7678            }),
7679        )
7680        .await;
7681        assert_eq!(second.status(), AxumStatusCode::OK);
7682        let second_body = response_json(second).await;
7683        assert_eq!(
7684            second_body["output"][0]["content"][0]["text"],
7685            "weather received"
7686        );
7687    }
7688
7689    #[tokio::test]
7690    async fn responses_route_marks_text_before_calls_as_commentary() {
7691        let request = || {
7692            json!({
7693                "model": "stub-model",
7694                "input": "Use the weather tool",
7695                "stream": false,
7696                "tools": [{
7697                    "type": "function",
7698                    "name": "weather",
7699                    "parameters": {"type": "object"}
7700                }]
7701            })
7702        };
7703        let sync = post_json(
7704            router_with_stub_api_response("", weather_tool_api_response_with_commentary()),
7705            "/v1/responses",
7706            request(),
7707        )
7708        .await;
7709        assert_eq!(sync.status(), AxumStatusCode::OK);
7710        let sync = response_json(sync).await;
7711        assert_eq!(sync["output"][0]["type"], "message");
7712        assert_eq!(sync["output"][0]["phase"], "commentary");
7713        assert_eq!(sync["output"][1]["type"], "function_call");
7714
7715        let mut stream_request = request();
7716        stream_request["stream"] = json!(true);
7717        let stream = post_json(
7718            router_with_stub_api_response("", weather_tool_api_response_with_commentary()),
7719            "/v1/responses",
7720            stream_request,
7721        )
7722        .await;
7723        assert_eq!(stream.status(), AxumStatusCode::OK);
7724        let body = response_text(stream).await;
7725        let events = responses_sse_json_events(&body);
7726        let message_added = events
7727            .iter()
7728            .find(|event| {
7729                event["type"] == "response.output_item.added" && event["item"]["type"] == "message"
7730            })
7731            .expect("message item added");
7732        assert!(message_added["item"].get("phase").is_none());
7733        let message_done = events
7734            .iter()
7735            .find(|event| {
7736                event["type"] == "response.output_item.done" && event["item"]["type"] == "message"
7737            })
7738            .expect("message item done");
7739        assert_eq!(message_done["item"]["phase"], "commentary");
7740        let terminal = events
7741            .iter()
7742            .find(|event| event["type"] == "response.completed")
7743            .expect("completed response");
7744        assert_eq!(terminal["response"]["output"][0]["phase"], "commentary");
7745        assert_eq!(terminal["response"]["output"][1]["type"], "function_call");
7746    }
7747
7748    #[tokio::test]
7749    async fn responses_route_accepts_real_caller_owned_second_turn_shape() {
7750        let response = post_json(
7751            router_with_stub("You first said hello."),
7752            "/v1/responses",
7753            json!({
7754                "model": "stub-model",
7755                "instructions": "Answer from the supplied history.",
7756                "input": [
7757                    {
7758                        "type": "message",
7759                        "role": "user",
7760                        "content": [{"type": "input_text", "text": "Hello"}]
7761                    },
7762                    {
7763                        "type": "message",
7764                        "role": "assistant",
7765                        "content": [{"type": "output_text", "text": "Hi there!"}]
7766                    },
7767                    {
7768                        "type": "reasoning",
7769                        "encrypted_content": null,
7770                        "summary": []
7771                    },
7772                    {
7773                        "type": "message",
7774                        "role": "user",
7775                        "content": [{"type": "input_text", "text": "What did I say first?"}]
7776                    }
7777                ],
7778                "store": false,
7779                "stream": false,
7780                "include": ["reasoning.encrypted_content"],
7781                "parallel_tool_calls": false,
7782                "prompt_cache_key": "thread-1",
7783                "reasoning": {"effort": "high", "summary": "auto"}
7784            }),
7785        )
7786        .await;
7787        assert_eq!(response.status(), AxumStatusCode::OK);
7788        let body = response_json(response).await;
7789        assert_eq!(
7790            body["output"][0]["content"][0]["text"],
7791            "You first said hello."
7792        );
7793        assert_eq!(body["parallel_tool_calls"], false);
7794        assert_eq!(body["prompt_cache_key"], "thread-1");
7795        assert_eq!(body["reasoning"]["effort"], "high");
7796    }
7797
7798    #[tokio::test]
7799    async fn responses_route_merges_instructions_with_leading_developer_message() {
7800        let template = ModelChatTemplate::new(
7801            "{% 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 %}",
7802            "strict-leading-system-template",
7803        );
7804        let response = post_json(
7805            router_with_stub_and_template("ok", template),
7806            "/v1/responses",
7807            json!({
7808                "model": "stub-model",
7809                "instructions": "Top-level instructions",
7810                "input": [
7811                    {"type": "message", "role": "developer", "content": "Developer instructions"},
7812                    {"type": "message", "role": "user", "content": "Hello"}
7813                ]
7814            }),
7815        )
7816        .await;
7817        assert_eq!(response.status(), AxumStatusCode::OK);
7818    }
7819
7820    #[tokio::test]
7821    async fn responses_route_adapts_interleaved_system_for_strict_template() {
7822        let template = ModelChatTemplate::new(
7823            "{% 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 }}",
7824            "strict-leading-system-template",
7825        );
7826        let response = post_json(
7827            router_with_stub_and_template("ok", template),
7828            "/v1/responses",
7829            json!({
7830                "model": "stub-model",
7831                "input": [
7832                    {"role": "system", "content": "Initial instructions"},
7833                    {"role": "user", "content": "Use the available tool"},
7834                    {"role": "developer", "content": "Deferred tool instructions"}
7835                ]
7836            }),
7837        )
7838        .await;
7839        assert_eq!(response.status(), AxumStatusCode::OK);
7840    }
7841
7842    #[tokio::test]
7843    async fn responses_route_keeps_phase_aligned_through_system_injection() {
7844        let template = ModelChatTemplate::new(
7845            "{% 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]",
7846            "phase-alignment-template",
7847        );
7848        let response = post_json(
7849            router_with_stub_and_template(r#"{"ok":true}"#, template),
7850            "/v1/responses",
7851            json!({
7852                "model": "stub-model",
7853                "instructions": "Top-level instructions",
7854                "input": [
7855                    {
7856                        "type": "message",
7857                        "role": "assistant",
7858                        "phase": "commentary",
7859                        "content": "I will inspect."
7860                    },
7861                    {"type": "message", "role": "user", "content": "Continue"}
7862                ],
7863                "text": {"format": {"type": "json_object"}}
7864            }),
7865        )
7866        .await;
7867        assert_eq!(response.status(), AxumStatusCode::OK);
7868    }
7869
7870    #[tokio::test]
7871    async fn responses_route_can_disable_interleaved_system_coalescing() {
7872        let template = ModelChatTemplate::new(
7873            "{% 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 %}",
7874            "strict-leading-system-template",
7875        );
7876        let mut engine = CapturingLlm::new();
7877        engine.config.model.model_id = ModelId::new("stub-model");
7878        let engine = Arc::new(engine);
7879        let router = AxumServer::from_state(
7880            AppState::default()
7881                .with_llm(engine.clone())
7882                .with_prompt_template(Some(template))
7883                .with_interleaved_system_coalescing(false),
7884        )
7885        .build_router();
7886        let response = post_json(
7887            router,
7888            "/v1/responses",
7889            json!({
7890                "model": "stub-model",
7891                "input": [
7892                    {"role": "system", "content": "Initial instructions"},
7893                    {"role": "user", "content": "Use the available tool"},
7894                    {"role": "developer", "content": "Deferred tool instructions"}
7895                ]
7896            }),
7897        )
7898        .await;
7899        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
7900        let body = response_json(response).await;
7901        assert_eq!(body["error"]["type"], "invalid_request_error");
7902        assert!(!engine.has_captured_request());
7903        assert!(
7904            body.to_string()
7905                .contains("System message must be at the beginning."),
7906            "{body}"
7907        );
7908    }
7909
7910    #[tokio::test]
7911    async fn chat_route_applies_and_can_disable_interleaved_system_coalescing() {
7912        let template = || {
7913            ModelChatTemplate::new(
7914                "{% 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 %}",
7915                "strict-leading-system-template",
7916            )
7917        };
7918        let request = || {
7919            json!({
7920                "model": "stub-model",
7921                "messages": [
7922                    {"role": "system", "content": "Initial instructions"},
7923                    {"role": "user", "content": "Use the available tool"},
7924                    {"role": "system", "content": "Deferred tool instructions"}
7925                ]
7926            })
7927        };
7928
7929        let enabled = post_json(
7930            router_with_stub_and_template("ok", template()),
7931            "/v1/chat/completions",
7932            request(),
7933        )
7934        .await;
7935        assert_eq!(enabled.status(), AxumStatusCode::OK);
7936
7937        let consecutive = post_json(
7938            router_with_stub_and_template("ok", template()),
7939            "/v1/chat/completions",
7940            json!({
7941                "model": "stub-model",
7942                "messages": [
7943                    {"role": "system", "content": "Initial instructions"},
7944                    {"role": "system", "content": "Deferred tool instructions"},
7945                    {"role": "user", "content": "Use the available tool"}
7946                ]
7947            }),
7948        )
7949        .await;
7950        assert_eq!(consecutive.status(), AxumStatusCode::OK);
7951
7952        let mut engine = CapturingLlm::new();
7953        engine.config.model.model_id = ModelId::new("stub-model");
7954        let engine = Arc::new(engine);
7955        let disabled_router = AxumServer::from_state(
7956            AppState::default()
7957                .with_llm(engine.clone())
7958                .with_prompt_template(Some(template()))
7959                .with_interleaved_system_coalescing(false),
7960        )
7961        .build_router();
7962        let disabled = post_json(disabled_router, "/v1/chat/completions", request()).await;
7963        assert_eq!(disabled.status(), AxumStatusCode::BAD_REQUEST);
7964        let body = response_json(disabled).await;
7965        assert_eq!(body["error"]["type"], "invalid_request_error");
7966        assert!(!engine.has_captured_request());
7967        assert!(
7968            body.to_string()
7969                .contains("System message must be at the beginning."),
7970            "{body}"
7971        );
7972    }
7973
7974    #[tokio::test]
7975    async fn responses_route_keeps_structured_output_to_one_leading_system_message() {
7976        let template = ModelChatTemplate::new(
7977            "{% 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 %}",
7978            "strict-leading-system-template",
7979        );
7980        let response = post_json(
7981            router_with_stub_and_template(r#"{"ok":true}"#, template),
7982            "/v1/responses",
7983            json!({
7984                "model": "stub-model",
7985                "instructions": "Top-level instructions",
7986                "input": [
7987                    {"type": "message", "role": "developer", "content": "Developer instructions"},
7988                    {"type": "message", "role": "user", "content": "Return JSON"}
7989                ],
7990                "text": {"format": {"type": "json_object"}}
7991            }),
7992        )
7993        .await;
7994        assert_eq!(response.status(), AxumStatusCode::OK);
7995        let body = response_json(response).await;
7996        assert_eq!(body["output"][0]["content"][0]["text"], r#"{"ok":true}"#);
7997    }
7998
7999    #[tokio::test]
8000    async fn responses_route_output_can_be_replayed_with_readable_reasoning() {
8001        let first = post_json(
8002            router_with_stub("<think>Checked the supplied facts.</think>\nFirst answer"),
8003            "/v1/responses",
8004            json!({
8005                "model": "stub-model",
8006                "input": "First question",
8007                "include": ["reasoning.encrypted_content"]
8008            }),
8009        )
8010        .await;
8011        assert_eq!(first.status(), AxumStatusCode::OK);
8012        let first_body = response_json(first).await;
8013        assert_eq!(first_body["output"][0]["type"], "reasoning");
8014        assert_eq!(
8015            first_body["output"][0]["content"][0]["text"],
8016            "Checked the supplied facts."
8017        );
8018        assert_eq!(first_body["output"][0]["encrypted_content"], Value::Null);
8019        assert_eq!(first_body["output"][1]["type"], "message");
8020
8021        let mut input = vec![json!({
8022            "type": "message",
8023            "role": "user",
8024            "content": [{"type": "input_text", "text": "First question"}]
8025        })];
8026        input.extend(first_body["output"].as_array().unwrap().iter().cloned());
8027        input.push(json!({
8028            "type": "message",
8029            "role": "user",
8030            "content": [{"type": "input_text", "text": "Continue"}]
8031        }));
8032        let second = post_json(
8033            router_with_stub("Second answer"),
8034            "/v1/responses",
8035            json!({"model": "stub-model", "input": input}),
8036        )
8037        .await;
8038        assert_eq!(second.status(), AxumStatusCode::OK);
8039        let second_body = response_json(second).await;
8040        assert_eq!(
8041            second_body["output"][0]["content"][0]["text"],
8042            "Second answer"
8043        );
8044    }
8045
8046    #[tokio::test]
8047    async fn responses_route_streams_reasoning_before_text_with_stable_indices() {
8048        let response = post_json(
8049            router_with_stub_stream_chunks(&["<think>inspect", " history</think>\nfinal"]),
8050            "/v1/responses",
8051            json!({
8052                "model": "stub-model",
8053                "input": "answer",
8054                "stream": true,
8055                "include": ["reasoning.encrypted_content"]
8056            }),
8057        )
8058        .await;
8059        assert_eq!(response.status(), AxumStatusCode::OK);
8060        let body = response_text(response).await;
8061        let events = responses_sse_json_events(&body);
8062        for (sequence, event) in events.iter().enumerate() {
8063            assert_eq!(
8064                event["sequence_number"], sequence,
8065                "Responses sequence numbers must be contiguous: {body}"
8066            );
8067        }
8068        for event in [
8069            "response.reasoning_text.delta",
8070            "response.reasoning_text.done",
8071            "response.output_text.delta",
8072            "response.completed",
8073        ] {
8074            assert!(
8075                body.contains(&format!("event: {event}")),
8076                "missing {event}: {body}"
8077            );
8078        }
8079        let reasoning_done = body
8080            .find("event: response.reasoning_text.done")
8081            .expect("reasoning done");
8082        let text_added = body[reasoning_done..]
8083            .find("event: response.output_item.added")
8084            .map(|offset| reasoning_done + offset)
8085            .expect("text item added");
8086        assert!(
8087            reasoning_done < text_added,
8088            "reasoning must finish before text: {body}"
8089        );
8090        assert!(
8091            body.contains("\"output_index\":0,\"content_index\":0,\"delta\":\"inspect"),
8092            "reasoning must use output index 0: {body}"
8093        );
8094        assert!(
8095            body.contains("\"output_index\":1,\"content_index\":0,\"delta\":\"final"),
8096            "text must use output index 1: {body}"
8097        );
8098        let reasoning_added = events
8099            .iter()
8100            .find(|event| {
8101                event["type"] == "response.output_item.added"
8102                    && event["item"]["type"] == "reasoning"
8103            })
8104            .expect("reasoning item added");
8105        assert_eq!(reasoning_added["item"]["status"], "in_progress");
8106        let reasoning_part_added = events
8107            .iter()
8108            .find(|event| {
8109                event["type"] == "response.content_part.added"
8110                    && event["part"]["type"] == "reasoning_text"
8111            })
8112            .expect("reasoning content part added");
8113        assert_eq!(reasoning_part_added["output_index"], 0);
8114        let reasoning_item_done = events
8115            .iter()
8116            .find(|event| {
8117                event["type"] == "response.output_item.done" && event["item"]["type"] == "reasoning"
8118            })
8119            .expect("reasoning item done");
8120        assert_eq!(reasoning_item_done["item"]["status"], "completed");
8121        let terminal = events
8122            .iter()
8123            .find(|event| event["type"] == "response.completed")
8124            .expect("terminal response");
8125        assert_eq!(
8126            terminal["response"]["output"][0],
8127            reasoning_item_done["item"]
8128        );
8129        assert!(
8130            body.contains("data: [DONE]"),
8131            "missing terminal marker: {body}"
8132        );
8133    }
8134
8135    #[tokio::test]
8136    async fn responses_route_streams_function_call_events() {
8137        let response = post_json(
8138            router_with_stub_api_response("", weather_tool_api_response()),
8139            "/v1/responses",
8140            json!({
8141                "model": "stub-model",
8142                "input": "Use the weather tool",
8143                "stream": true,
8144                "tools": [{
8145                    "type": "function",
8146                    "name": "weather",
8147                    "parameters": {"type": "object"}
8148                }]
8149            }),
8150        )
8151        .await;
8152        assert_eq!(response.status(), AxumStatusCode::OK);
8153        let body = response_text(response).await;
8154        assert!(
8155            body.contains("event: response.function_call_arguments.delta"),
8156            "missing function delta: {body}"
8157        );
8158        assert!(
8159            body.contains("event: response.function_call_arguments.done"),
8160            "missing function done: {body}"
8161        );
8162        assert!(
8163            body.contains("\"call_id\":\"call_1\""),
8164            "missing call id: {body}"
8165        );
8166        assert_eq!(body.matches("event: response.completed").count(), 1);
8167    }
8168
8169    #[tokio::test]
8170    async fn responses_route_round_trips_namespace_identity_without_leaking_chat_alias() {
8171        let namespace_tool = json!({
8172            "type": "namespace",
8173            "name": "collaboration",
8174            "description": "Agent coordination tools",
8175            "tools": [{
8176                "type": "function",
8177                "name": "wait_agent",
8178                "parameters": {"type": "object"}
8179            }]
8180        });
8181        let sync = post_json(
8182            router_with_stub_api_response("", namespaced_tool_api_response()),
8183            "/v1/responses",
8184            json!({
8185                "model": "stub-model",
8186                "input": "Wait for the agent",
8187                "tools": [namespace_tool.clone()]
8188            }),
8189        )
8190        .await;
8191        assert_eq!(sync.status(), AxumStatusCode::OK);
8192        let sync_body = response_json(sync).await;
8193        assert_eq!(sync_body["output"][0]["type"], "function_call");
8194        assert_eq!(sync_body["output"][0]["namespace"], "collaboration");
8195        assert_eq!(sync_body["output"][0]["name"], "wait_agent");
8196
8197        let stream = post_json(
8198            router_with_stub_api_response("", namespaced_tool_api_response()),
8199            "/v1/responses",
8200            json!({
8201                "model": "stub-model",
8202                "input": "Wait for the agent",
8203                "tools": [namespace_tool],
8204                "stream": true
8205            }),
8206        )
8207        .await;
8208        assert_eq!(stream.status(), AxumStatusCode::OK);
8209        let stream_body = response_text(stream).await;
8210        let events = responses_sse_json_events(&stream_body);
8211        let function_events = events
8212            .iter()
8213            .filter(|event| {
8214                event["item"]["type"] == "function_call"
8215                    || event["type"] == "response.function_call_arguments.done"
8216            })
8217            .collect::<Vec<_>>();
8218        assert!(!function_events.is_empty());
8219        for event in function_events {
8220            let value = event.get("item").unwrap_or(event);
8221            assert_eq!(value["namespace"], "collaboration");
8222            assert_eq!(value["name"], "wait_agent");
8223        }
8224        assert!(stream_body.contains("data: [DONE]"));
8225        assert!(!stream_body.contains("collaboration__wait_agent"));
8226    }
8227
8228    #[tokio::test]
8229    async fn responses_route_enforces_parallel_tool_call_constraint() {
8230        let tools = json!([
8231            {"type": "function", "name": "weather", "parameters": {"type": "object"}},
8232            {"type": "function", "name": "clock", "parameters": {"type": "object"}}
8233        ]);
8234        let sync = post_json(
8235            router_with_stub_api_response("", two_tool_api_response()),
8236            "/v1/responses",
8237            json!({
8238                "model": "stub-model",
8239                "input": "Use both tools",
8240                "tools": tools.clone(),
8241                "parallel_tool_calls": false
8242            }),
8243        )
8244        .await;
8245        assert_eq!(sync.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
8246
8247        let stream = post_json(
8248            router_with_stub_api_response("", two_tool_api_response()),
8249            "/v1/responses",
8250            json!({
8251                "model": "stub-model",
8252                "input": "Use both tools",
8253                "tools": tools,
8254                "parallel_tool_calls": false,
8255                "stream": true
8256            }),
8257        )
8258        .await;
8259        assert_eq!(stream.status(), AxumStatusCode::OK);
8260        let body = response_text(stream).await;
8261        assert!(
8262            body.contains("event: response.failed"),
8263            "missing failure: {body}"
8264        );
8265        assert!(
8266            !body.contains("event: response.completed"),
8267            "must not complete: {body}"
8268        );
8269        assert!(
8270            body.contains("data: [DONE]"),
8271            "missing terminal marker: {body}"
8272        );
8273    }
8274
8275    #[tokio::test]
8276    async fn responses_route_streams_incomplete_terminal_event() {
8277        let response = post_json(
8278            router_with_stub_finish_reason("partial", FinishReason::Length),
8279            "/v1/responses",
8280            json!({"model": "stub-model", "input": "answer", "stream": true}),
8281        )
8282        .await;
8283        assert_eq!(response.status(), AxumStatusCode::OK);
8284        let body = response_text(response).await;
8285        assert!(
8286            body.contains("event: response.incomplete"),
8287            "missing incomplete terminal event: {body}"
8288        );
8289        assert!(
8290            !body.contains("event: response.completed"),
8291            "incomplete response must not emit completed: {body}"
8292        );
8293        assert!(
8294            body.contains("data: [DONE]"),
8295            "missing terminal marker: {body}"
8296        );
8297        let events = responses_sse_json_events(&body);
8298        let output_done = events
8299            .iter()
8300            .find(|event| event["type"] == "response.output_item.done")
8301            .expect("incomplete output item done event");
8302        assert_eq!(output_done["item"]["status"], "incomplete");
8303        let terminal = events
8304            .iter()
8305            .find(|event| event["type"] == "response.incomplete")
8306            .expect("incomplete terminal event");
8307        assert_eq!(terminal["response"]["output"][0]["status"], "incomplete");
8308    }
8309
8310    #[tokio::test]
8311    async fn responses_route_marks_sync_length_output_incomplete() {
8312        let response = post_json(
8313            router_with_stub_finish_reason("partial", FinishReason::Length),
8314            "/v1/responses",
8315            json!({"model": "stub-model", "input": "answer"}),
8316        )
8317        .await;
8318        assert_eq!(response.status(), AxumStatusCode::OK);
8319        let body = response_json(response).await;
8320        assert_eq!(body["status"], "incomplete");
8321        assert_eq!(body["output"][0]["status"], "incomplete");
8322    }
8323
8324    #[tokio::test]
8325    async fn responses_route_rejects_state_and_non_function_tools() {
8326        for (extra, param) in [
8327            (json!({"store": true}), "store"),
8328            (
8329                json!({"previous_response_id": "resp_previous"}),
8330                "previous_response_id",
8331            ),
8332            (
8333                json!({"tools": [{"type": "mcp", "server_label": "docs"}]}),
8334                "tools[0].type",
8335            ),
8336        ] {
8337            let mut body = json!({"model": "stub-model", "input": "hello"});
8338            body.as_object_mut()
8339                .unwrap()
8340                .extend(extra.as_object().unwrap().clone());
8341            let response = post_json(router_with_stub("unused"), "/v1/responses", body).await;
8342            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8343            let error = response_json(response).await;
8344            assert_eq!(error["error"]["param"], param, "error: {error}");
8345        }
8346    }
8347
8348    #[tokio::test]
8349    async fn responses_mvp_keeps_chat_completions_route_working() {
8350        let response = post_json(
8351            router_with_stub("chat still works"),
8352            "/v1/chat/completions",
8353            json!({
8354                "model": "stub-model",
8355                "messages": [{"role": "user", "content": "hello"}]
8356            }),
8357        )
8358        .await;
8359        assert_eq!(response.status(), AxumStatusCode::OK);
8360        let body = response_json(response).await;
8361        assert_eq!(body["choices"][0]["message"]["content"], "chat still works");
8362    }
8363
8364    #[test]
8365    fn sanitized_chat_request_body_redacts_user_text_and_secret_metadata() {
8366        let request = chat_request(json!({
8367            "messages": [{"role": "user", "content": "private prompt"}],
8368            "metadata": {"api_key": "should-not-survive"},
8369            "stream": true
8370        }));
8371        let body = sanitized_chat_request_body(&request);
8372        assert_eq!(body["model"], "stub-model");
8373        assert_eq!(body["stream"], true);
8374        assert_eq!(body["messages"][0]["role"], "user");
8375        assert_eq!(body["messages"][0]["content"], "[redacted]");
8376        assert_eq!(body["messages"][0]["content_redacted"], true);
8377        assert_eq!(body["messages"][0]["content_chars"], 14);
8378        assert_eq!(body["metadata"]["api_key"], "[redacted]");
8379    }
8380
8381    #[test]
8382    fn admission_health_prefers_runtime_authority_over_preflight_estimate() {
8383        let engine_status = EngineStatus {
8384            is_ready: true,
8385            loaded_models: Vec::new(),
8386            active_requests: 2,
8387            queued_requests: 1,
8388            memory_usage: MemoryUsage {
8389                total_bytes: 0,
8390                used_bytes: 0,
8391                free_bytes: 0,
8392                gpu_memory_bytes: None,
8393                cpu_memory_bytes: None,
8394                cache_memory_bytes: 0,
8395                utilization_percent: 0.0,
8396            },
8397            uptime_seconds: 0,
8398            last_heartbeat: chrono::Utc::now(),
8399            version: "test".to_owned(),
8400        };
8401        let runtime = ferrum_types::ExecutorAdmissionSnapshot::new(
8402            ferrum_types::ExecutionResourceAuthority::PlanRuntime,
8403            ferrum_types::ExecutorAdmissionLimits::new(32, 4096).unwrap(),
8404            2,
8405            7,
8406            23,
8407            None,
8408            Some(3),
8409        )
8410        .unwrap();
8411        let admission = admission_health_json(
8412            &engine_status,
8413            &EngineMetrics::default(),
8414            &json!({
8415                "admission": {
8416                    "effective_max_concurrent": 16,
8417                    "scheduler_policy": "continuous"
8418                }
8419            }),
8420            Some(&runtime),
8421            None,
8422        );
8423
8424        assert_eq!(admission["source"], "runtime_executor");
8425        assert_eq!(admission["runtime_snapshot_available"], true);
8426        assert_eq!(admission["resource_authority"], "plan_runtime");
8427        assert_eq!(admission["effective_max_concurrent"], 32);
8428        assert_eq!(admission["maximum_active_sequences"], 32);
8429        assert_eq!(admission["maximum_scheduled_tokens"], 4096);
8430        assert_eq!(admission["preflight_effective_max_concurrent"], 16);
8431        assert_eq!(admission["active_sequences"], 30);
8432        assert_eq!(admission["active_prefill"], 7);
8433        assert_eq!(admission["active_decode"], 23);
8434        assert!(admission["current_batch_size"].is_null());
8435        assert_eq!(admission["queue_depth"], 2);
8436        assert_eq!(admission["capacity_blocked_requests"], 3);
8437    }
8438
8439    #[test]
8440    fn admission_health_surfaces_runtime_contract_failure_without_preflight_fallback() {
8441        let engine_status = EngineStatus {
8442            is_ready: true,
8443            loaded_models: Vec::new(),
8444            active_requests: 32,
8445            queued_requests: 1,
8446            memory_usage: MemoryUsage {
8447                total_bytes: 0,
8448                used_bytes: 0,
8449                free_bytes: 0,
8450                gpu_memory_bytes: None,
8451                cpu_memory_bytes: None,
8452                cache_memory_bytes: 0,
8453                utilization_percent: 0.0,
8454            },
8455            uptime_seconds: 0,
8456            last_heartbeat: chrono::Utc::now(),
8457            version: "test".to_owned(),
8458        };
8459        let admission = admission_health_json(
8460            &engine_status,
8461            &EngineMetrics::default(),
8462            &json!({
8463                "admission": {
8464                    "effective_max_concurrent": 16,
8465                    "scheduler_policy": "continuous"
8466                }
8467            }),
8468            None,
8469            Some("active phase count exceeded the runtime ceiling"),
8470        );
8471
8472        assert_eq!(admission["source"], "runtime_error");
8473        assert_eq!(admission["runtime_snapshot_available"], false);
8474        assert_eq!(admission["preflight_effective_max_concurrent"], 16);
8475        assert!(admission["effective_max_concurrent"].is_null());
8476        assert!(admission["queue_depth"].is_null());
8477        assert_eq!(
8478            admission["runtime_contract_error"],
8479            "active phase count exceeded the runtime ceiling"
8480        );
8481    }
8482
8483    #[tokio::test]
8484    async fn route_health_includes_runtime_config_snapshot() {
8485        let response = get(router_with_stub("ok"), "/health").await;
8486        assert_eq!(response.status(), AxumStatusCode::OK);
8487        let body = response_json(response).await;
8488        assert_eq!(body["status"], "healthy");
8489        assert!(body["config"]["entries"].is_array(), "body: {body}");
8490        assert_eq!(body["auto_config"]["schema_version"], 1);
8491        assert!(body["auto_config"]["entries"].is_array(), "body: {body}");
8492        assert!(body["auto_config"]["admission"].is_object(), "body: {body}");
8493        assert_eq!(body["admission"]["schema_version"], 2);
8494        assert!(body["admission"]["effective_max_concurrent"].is_number());
8495        assert!(body["admission"]["queue_depth"].is_number());
8496        assert!(body["admission"]["active_sequences"].is_number());
8497        assert!(body["admission"]["active_prefill"].is_null());
8498        assert!(body["admission"]["active_decode"].is_null());
8499        assert!(body["admission"]["current_batch_size"].is_null());
8500        assert!(body["admission"]["rejected_requests_total"].is_number());
8501        assert!(body["admission"]["failed_requests_total"].is_number());
8502        assert!(body["admission"]["completed_requests_total"].is_number());
8503        assert!(body["admission"]["avg_queue_wait_time_ms"].is_number());
8504        assert!(body["scheduler"]["avg_wait_time_ms"].is_number());
8505        assert!(body["scheduler"]["scheduling_time_ms"].is_number());
8506        assert!(body["scheduler"]["model_execution_time_ms"].is_number());
8507        assert!(body["scheduler"]["iteration_lock_wait_time_ms"].is_number());
8508        assert!(
8509            body["auto_config"]["decisions"].is_array() || body["auto_config"]["error"].is_string(),
8510            "body: {body}"
8511        );
8512    }
8513
8514    #[tokio::test]
8515    async fn route_metrics_includes_admission_counters() {
8516        let response = get(router_with_stub("ok"), "/metrics").await;
8517        assert_eq!(response.status(), AxumStatusCode::OK);
8518        let body = response_text(response).await;
8519        for metric in [
8520            "ferrum_admission_runtime_snapshot_available",
8521            "ferrum_admission_effective_max_concurrent",
8522            "ferrum_admission_queue_depth",
8523            "ferrum_admission_active_sequences",
8524            "ferrum_admission_rejected_requests_total",
8525            "ferrum_admission_failed_requests_total",
8526            "ferrum_admission_completed_requests_total",
8527        ] {
8528            assert!(body.contains(metric), "missing {metric}:\n{body}");
8529        }
8530        for unavailable_metric in [
8531            "ferrum_admission_maximum_active_sequences ",
8532            "ferrum_admission_maximum_scheduled_tokens ",
8533            "ferrum_admission_capacity_blocked_requests ",
8534            "ferrum_admission_active_prefill ",
8535            "ferrum_admission_active_decode ",
8536            "ferrum_admission_current_batch_size ",
8537        ] {
8538            assert!(
8539                !body.contains(unavailable_metric),
8540                "unknown metric was encoded as a real value: {unavailable_metric}\n{body}"
8541            );
8542        }
8543    }
8544
8545    #[tokio::test]
8546    async fn route_health_includes_engine_lora_metrics_snapshot() {
8547        let router = AxumServer::from_llm(Arc::new(StubLlm::with_lora_metrics(
8548            "ok",
8549            json!({
8550                "enabled": true,
8551                "adapter_count": 1,
8552                "active_cache_bindings": 0,
8553                "projection_applications": 7,
8554                "position": "real-inference",
8555                "source": "test-lora",
8556            }),
8557        )))
8558        .with_lora_adapters(
8559            "stub-model",
8560            vec![LoraAdapterModel::new(
8561                "sql",
8562                "stub-model:sql",
8563                "/tmp/sql-adapter",
8564            )],
8565        )
8566        .unwrap()
8567        .build_router();
8568        let response = get(router, "/health").await;
8569        assert_eq!(response.status(), AxumStatusCode::OK);
8570        let body = response_json(response).await;
8571        assert_eq!(body["lora"]["enabled"], true);
8572        assert_eq!(body["lora"]["adapter_count"], 1);
8573        assert_eq!(body["lora"]["projection_applications"], 7);
8574        assert_eq!(body["lora"]["position"], "real-inference");
8575        assert_eq!(body["lora"]["source"], "test-lora");
8576    }
8577
8578    #[tokio::test]
8579    async fn route_health_includes_engine_execution_attribution_snapshot() {
8580        let router = AxumServer::from_llm(Arc::new(StubLlm::with_execution_attribution(
8581            "ok",
8582            json!({
8583                "schema": "ferrum.vnext.provider-attribution.v1",
8584                "attribution_basis": "resolved_plan_and_completed_static_initialization",
8585                "provider_attribution": {
8586                    "expected_quant_tensor_count": 400,
8587                    "attributed_quant_tensor_count": 400,
8588                    "expected_operation_count": 3,
8589                    "attributed_operation_count": 3,
8590                    "expected_item_count": 403,
8591                    "attributed_item_count": 403,
8592                    "percent": 100.0,
8593                    "denominator_sha256": "5e366997e15e1a94d90b1ae07281269e8a46f75904306564d56354c8ebea2e4e"
8594                },
8595                "fallback_counts": {"silent": 0, "dense": 0, "legacy": 0}
8596            }),
8597        )))
8598        .build_router();
8599        let response = get(router, "/health").await;
8600        assert_eq!(response.status(), AxumStatusCode::OK);
8601        let body = response_json(response).await;
8602        assert_eq!(
8603            body["execution_attribution"]["provider_attribution"]["expected_item_count"],
8604            403
8605        );
8606        assert_eq!(
8607            body["execution_attribution"]["provider_attribution"]["denominator_sha256"],
8608            "5e366997e15e1a94d90b1ae07281269e8a46f75904306564d56354c8ebea2e4e"
8609        );
8610        assert_eq!(
8611            body["execution_attribution"]["fallback_counts"],
8612            json!({"silent": 0, "dense": 0, "legacy": 0})
8613        );
8614    }
8615
8616    #[tokio::test]
8617    async fn route_models_lists_loaded_stub_model() {
8618        let response = get(router_with_stub("ok"), "/v1/models").await;
8619        assert_eq!(response.status(), AxumStatusCode::OK);
8620        let body = response_json(response).await;
8621        assert_eq!(body["object"], "list");
8622        let data = body["data"].as_array().expect("models data array");
8623        assert_eq!(data.len(), 1, "body: {body}");
8624        assert_eq!(data[0]["id"], "stub-model");
8625        assert_eq!(data[0]["object"], "model");
8626        assert_eq!(data[0]["owned_by"], "ferrum");
8627        assert!(data[0]["created"].as_u64().unwrap_or_default() > 0);
8628        assert_eq!(data[0]["modalities"], json!(["text"]));
8629        assert!(data[0]["permission"].as_array().unwrap().is_empty());
8630        assert!(data[0]["root"].is_null());
8631        assert!(data[0]["parent"].is_null());
8632        assert!(data[0].get("max_model_len").is_none());
8633    }
8634
8635    #[tokio::test]
8636    async fn route_chat_public_alias_maps_to_internal_model_and_is_echoed() {
8637        let engine = Arc::new(CapturingLlm::new());
8638        let registry = ServedModelRegistry::try_new(
8639            "qwen3",
8640            ServedModelKind::Llm,
8641            vec!["served-alias".to_string(), "secondary-alias".to_string()],
8642            vec![],
8643        )
8644        .unwrap();
8645        let router = AxumServer::from_llm(engine.clone())
8646            .with_served_model_registry(registry)
8647            .build_router();
8648        let response = post_json(
8649            router,
8650            "/v1/chat/completions",
8651            json!({
8652                "model": "secondary-alias",
8653                "messages": [{"role": "user", "content": "Say hi"}],
8654                "max_tokens": 8
8655            }),
8656        )
8657        .await;
8658
8659        assert_eq!(response.status(), AxumStatusCode::OK);
8660        let body = response_json(response).await;
8661        assert_eq!(body["model"], "secondary-alias");
8662        assert_eq!(engine.last_request().model_id, ModelId::new("qwen3"));
8663    }
8664
8665    #[tokio::test]
8666    async fn route_models_lists_public_aliases_without_internal_model_id() {
8667        let registry = ServedModelRegistry::try_new(
8668            "qwen3",
8669            ServedModelKind::Llm,
8670            vec!["served-alias".to_string(), "secondary-alias".to_string()],
8671            vec![],
8672        )
8673        .unwrap();
8674        let router = AxumServer::from_llm(Arc::new(CapturingLlm::new()))
8675            .with_served_model_registry(registry)
8676            .build_router();
8677        let body = response_json(get(router, "/v1/models").await).await;
8678        let ids = body["data"]
8679            .as_array()
8680            .unwrap()
8681            .iter()
8682            .map(|entry| entry["id"].as_str().unwrap())
8683            .collect::<Vec<_>>();
8684
8685        assert_eq!(ids, vec!["served-alias", "secondary-alias"]);
8686        assert!(!ids.contains(&"qwen3"));
8687        assert!(body["data"]
8688            .as_array()
8689            .unwrap()
8690            .iter()
8691            .all(|entry| entry["modalities"] == json!(["text"])));
8692    }
8693
8694    #[tokio::test]
8695    async fn route_models_lists_embedding_registry_capabilities() {
8696        let body = response_json(get(router_with_stub_embed(), "/v1/models").await).await;
8697        let data = body["data"].as_array().unwrap();
8698
8699        assert_eq!(data.len(), 1);
8700        assert_eq!(data[0]["id"], "stub-embed");
8701        assert_eq!(data[0]["modalities"], json!(["text", "image"]));
8702        assert!(data[0].get("max_model_len").is_none());
8703    }
8704
8705    #[tokio::test]
8706    async fn route_models_reports_engine_capacity_for_public_aliases_and_adapters() {
8707        let capacity = 3072;
8708        let engine = StubLlm {
8709            context_capacity: Some(capacity),
8710            ..StubLlm::new("ok")
8711        };
8712        let registry = ServedModelRegistry::try_new(
8713            "stub-model",
8714            ServedModelKind::Llm,
8715            vec!["public-model".to_owned(), "second-alias".to_owned()],
8716            vec![LoraAdapterModel::new(
8717                "sql",
8718                "public-model:sql",
8719                "/tmp/adapter",
8720            )],
8721        )
8722        .unwrap();
8723        let router = AxumServer::from_llm(Arc::new(engine))
8724            .with_served_model_registry(registry)
8725            .build_router();
8726        let body = response_json(get(router, "/v1/models").await).await;
8727        let entries = body["data"].as_array().unwrap();
8728        assert_eq!(entries.len(), 3);
8729        for entry in entries {
8730            assert_eq!(entry["max_model_len"], capacity);
8731        }
8732    }
8733
8734    #[tokio::test]
8735    async fn route_models_lists_startup_lora_adapters() {
8736        let router = AxumServer::from_llm(Arc::new(StubLlm::new("ok")))
8737            .with_lora_adapters(
8738                "stub-model",
8739                vec![LoraAdapterModel::new(
8740                    "sql",
8741                    "stub-model:sql",
8742                    "/tmp/sql-adapter",
8743                )],
8744            )
8745            .unwrap()
8746            .build_router();
8747        let response = get(router, "/v1/models").await;
8748        assert_eq!(response.status(), AxumStatusCode::OK);
8749        let body = response_json(response).await;
8750        let data = body["data"].as_array().expect("models data array");
8751        let ids: Vec<_> = data
8752            .iter()
8753            .map(|item| item["id"].as_str().unwrap_or_default())
8754            .collect();
8755        assert!(ids.contains(&"stub-model"), "body: {body}");
8756        assert!(ids.contains(&"stub-model:sql"), "body: {body}");
8757        let adapter = data
8758            .iter()
8759            .find(|item| item["id"] == "stub-model:sql")
8760            .expect("adapter model");
8761        assert_eq!(adapter["root"], "stub-model");
8762        assert_eq!(adapter["parent"], "stub-model");
8763        assert_eq!(adapter["modalities"], json!(["text"]));
8764    }
8765
8766    #[tokio::test]
8767    async fn route_chat_lora_adapter_maps_internal_request_to_base_model() {
8768        let (router, engine) = router_with_capturing_lora_llm();
8769        let response = post_json(
8770            router,
8771            "/v1/chat/completions",
8772            json!({
8773                "model": "qwen3:sql",
8774                "messages": [{"role": "user", "content": "Say hi"}],
8775                "max_tokens": 8,
8776                "temperature": 0.0
8777            }),
8778        )
8779        .await;
8780        assert_eq!(response.status(), AxumStatusCode::OK);
8781        let body = response_json(response).await;
8782        assert_eq!(body["model"], "qwen3:sql");
8783        let captured = engine.last_request();
8784        assert_eq!(captured.model_id, ModelId::new("qwen3"));
8785        assert_eq!(captured.metadata["ferrum_lora_adapter"], "sql");
8786        assert_eq!(captured.metadata["ferrum_lora_model_id"], "qwen3:sql");
8787    }
8788
8789    #[tokio::test]
8790    async fn route_chat_base_model_still_uses_base_path_with_lora_loaded() {
8791        let (router, engine) = router_with_capturing_lora_llm();
8792        let response = post_json(
8793            router,
8794            "/v1/chat/completions",
8795            json!({
8796                "model": "qwen3",
8797                "messages": [{"role": "user", "content": "Say hi"}],
8798                "max_tokens": 8,
8799                "temperature": 0.0
8800            }),
8801        )
8802        .await;
8803        assert_eq!(response.status(), AxumStatusCode::OK);
8804        let captured = engine.last_request();
8805        assert_eq!(captured.model_id, ModelId::new("qwen3"));
8806        assert!(!captured.metadata.contains_key("ferrum_lora_adapter"));
8807    }
8808
8809    #[tokio::test]
8810    async fn route_chat_unknown_lora_adapter_returns_openai_model_error() {
8811        let (router, _) = router_with_capturing_lora_llm();
8812        let response = post_json(
8813            router,
8814            "/v1/chat/completions",
8815            json!({
8816                "model": "qwen3:missing",
8817                "messages": [{"role": "user", "content": "Say hi"}],
8818                "max_tokens": 8
8819            }),
8820        )
8821        .await;
8822        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8823        let body = response_json(response).await;
8824        assert_eq!(body["error"]["type"], "invalid_request_error");
8825        assert_eq!(body["error"]["param"], "model");
8826        assert!(
8827            body["error"]["message"]
8828                .as_str()
8829                .unwrap_or_default()
8830                .contains("unknown model"),
8831            "body: {body}"
8832        );
8833    }
8834
8835    #[tokio::test]
8836    async fn route_chat_unknown_served_model_returns_openai_model_error() {
8837        let engine = Arc::new(CapturingLlm::new());
8838        let router = AxumServer::from_llm(engine.clone()).build_router();
8839        let response = post_json(
8840            router,
8841            "/v1/chat/completions",
8842            json!({
8843                "model": "not-a-loaded-model",
8844                "messages": [{"role": "user", "content": "Say hi"}],
8845                "max_tokens": 8
8846            }),
8847        )
8848        .await;
8849        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8850        let body = response_json(response).await;
8851        assert_eq!(body["error"]["type"], "invalid_request_error");
8852        assert_eq!(body["error"]["param"], "model");
8853        assert!(
8854            body["error"]["message"]
8855                .as_str()
8856                .unwrap_or_default()
8857                .contains("unknown model"),
8858            "body: {body}"
8859        );
8860        assert!(!engine.has_captured_request());
8861    }
8862
8863    #[tokio::test]
8864    async fn route_models_without_engine_returns_empty_list() {
8865        let response = get(router_without_llm(), "/v1/models").await;
8866        assert_eq!(response.status(), AxumStatusCode::OK);
8867        let body = response_json(response).await;
8868        assert_eq!(body["object"], "list");
8869        assert!(body["data"].as_array().unwrap().is_empty(), "body: {body}");
8870    }
8871
8872    #[tokio::test]
8873    async fn route_basic_chat_contract_uses_stub_engine() {
8874        let response = post_json(
8875            router_with_stub("hello"),
8876            "/v1/chat/completions",
8877            json!({
8878                "model": "stub-model",
8879                "messages": [{"role": "user", "content": "Say hi"}],
8880                "max_tokens": 8,
8881                "temperature": 0.0
8882            }),
8883        )
8884        .await;
8885        assert_eq!(response.status(), AxumStatusCode::OK);
8886        let body = response_json(response).await;
8887        assert_eq!(body["object"], "chat.completion");
8888        assert_eq!(body["choices"][0]["message"]["role"], "assistant");
8889        assert_eq!(body["choices"][0]["message"]["content"], "hello");
8890        assert_eq!(body["usage"]["prompt_tokens"], 7);
8891        assert_eq!(body["usage"]["completion_tokens"], 2);
8892    }
8893
8894    #[tokio::test]
8895    async fn route_chat_serializes_structured_tool_call_response() {
8896        let response = post_json(
8897            router_with_stub_api_response(
8898                "",
8899                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
8900                    message: ferrum_types::ApiChatMessage {
8901                        role: ferrum_types::ApiMessageRole::Assistant,
8902                        content: String::new(),
8903                        name: None,
8904                        tool_calls: vec![ferrum_types::ApiToolCall {
8905                            id: "call_1".to_string(),
8906                            tool_type: "function".to_string(),
8907                            function: ferrum_types::ApiFunctionCall {
8908                                name: "weather".to_string(),
8909                                arguments: "{\"city\":\"Paris\"}".to_string(),
8910                            },
8911                        }],
8912                        tool_call_id: None,
8913                        function_call: None,
8914                    },
8915                    finish_reason: Some("tool_calls".to_string()),
8916                }),
8917            ),
8918            "/v1/chat/completions",
8919            json!({
8920                "model": "stub-model",
8921                "messages": [{"role": "user", "content": "Use the weather tool."}],
8922                "tools": [{
8923                    "type": "function",
8924                    "function": {
8925                        "name": "weather",
8926                        "parameters": {
8927                            "type": "object",
8928                            "properties": {"city": {"type": "string"}},
8929                            "required": ["city"]
8930                        }
8931                    }
8932                }],
8933                "tool_choice": "auto"
8934            }),
8935        )
8936        .await;
8937        assert_eq!(response.status(), AxumStatusCode::OK);
8938        let body = response_json(response).await;
8939        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8940        assert_eq!(
8941            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8942            "weather"
8943        );
8944        assert_eq!(
8945            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8946            "{\"city\":\"Paris\"}"
8947        );
8948    }
8949
8950    #[tokio::test]
8951    async fn route_chat_preserves_length_over_structured_tool_response() {
8952        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
8953        let response = post_json(
8954            router_with_stub_api_response_and_finish_reason(
8955                generated,
8956                weather_tool_api_response(),
8957                FinishReason::Length,
8958            ),
8959            "/v1/chat/completions",
8960            json!({
8961                "model": "stub-model",
8962                "messages": [{"role": "user", "content": "Use the weather tool."}],
8963                "tools": [{
8964                    "type": "function",
8965                    "function": {"name": "weather", "parameters": {"type": "object"}}
8966                }],
8967                "tool_choice": "auto"
8968            }),
8969        )
8970        .await;
8971        assert_eq!(response.status(), AxumStatusCode::OK);
8972        let body = response_json(response).await;
8973        assert_eq!(body["choices"][0]["finish_reason"], "length");
8974        assert_eq!(body["choices"][0]["message"]["content"], generated);
8975        assert!(body["choices"][0]["message"]["tool_calls"].is_null());
8976    }
8977
8978    #[tokio::test]
8979    async fn route_chat_serializes_generated_tool_call_json_when_engine_returns_text_only() {
8980        let response = post_json(
8981            router_with_stub(
8982                r#"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"weather","arguments":{"city":"Paris"}}}]}"#,
8983            ),
8984            "/v1/chat/completions",
8985            json!({
8986                "model": "stub-model",
8987                "messages": [{"role": "user", "content": "Use the weather tool."}],
8988                "tools": [{
8989                    "type": "function",
8990                    "function": {
8991                        "name": "weather",
8992                        "parameters": {
8993                            "type": "object",
8994                            "properties": {"city": {"type": "string"}},
8995                            "required": ["city"]
8996                        }
8997                    }
8998                }],
8999                "tool_choice": "auto"
9000            }),
9001        )
9002        .await;
9003        assert_eq!(response.status(), AxumStatusCode::OK);
9004        let body = response_json(response).await;
9005        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9006        assert_eq!(body["choices"][0]["message"]["content"], "");
9007        assert_eq!(
9008            body["choices"][0]["message"]["tool_calls"][0]["id"],
9009            "call_1"
9010        );
9011        assert_eq!(
9012            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9013            "weather"
9014        );
9015        assert_eq!(
9016            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9017            "{\"city\":\"Paris\"}"
9018        );
9019    }
9020
9021    #[tokio::test]
9022    async fn route_chat_serializes_qwen3_function_parameters_tool_json() {
9023        let response = post_json(
9024            router_with_stub(
9025                r#"{"function":"get_weather","parameters":{"city":"北京","unit":"c"}}"#,
9026            ),
9027            "/v1/chat/completions",
9028            json!({
9029                "model": "stub-model",
9030                "messages": [{"role": "user", "content": "北京现在天气怎么样?"}],
9031                "tools": [{
9032                    "type": "function",
9033                    "function": {
9034                        "name": "get_weather",
9035                        "parameters": {
9036                            "type": "object",
9037                            "properties": {
9038                                "city": {"type": "string"},
9039                                "unit": {"type": "string", "enum": ["c", "f"]}
9040                            },
9041                            "required": ["city"]
9042                        }
9043                    }
9044                }],
9045                "tool_choice": "auto"
9046            }),
9047        )
9048        .await;
9049        assert_eq!(response.status(), AxumStatusCode::OK);
9050        let body = response_json(response).await;
9051        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9052        assert_eq!(body["choices"][0]["message"]["content"], "");
9053        assert_eq!(
9054            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9055            "get_weather"
9056        );
9057        assert_eq!(
9058            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9059            "{\"city\":\"北京\",\"unit\":\"c\"}"
9060        );
9061    }
9062
9063    #[tokio::test]
9064    async fn route_chat_uses_template_tool_protocol_for_function_parameter_xml() {
9065        let template = ModelChatTemplate::new(
9066            "{% 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 %}",
9067            "function-parameter-xml-template",
9068        );
9069        let response = post_json(
9070            router_with_stub_and_template(
9071                "<tool_call>\n<function=get_weather>\n<parameter=city>\n北京\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>\n</tool_call>",
9072                template,
9073            ),
9074            "/v1/chat/completions",
9075            json!({
9076                "model": "stub-model",
9077                "messages": [{"role": "user", "content": "请调用 get_weather 查询北京天气。"}],
9078                "tools": [{
9079                    "type": "function",
9080                    "function": {
9081                        "name": "get_weather",
9082                        "parameters": {
9083                            "type": "object",
9084                            "properties": {
9085                                "city": {"type": "string"},
9086                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
9087                            },
9088                            "required": ["city"]
9089                        }
9090                    }
9091                }]
9092            }),
9093        )
9094        .await;
9095
9096        assert_eq!(response.status(), AxumStatusCode::OK);
9097        let body = response_json(response).await;
9098        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9099        assert_eq!(
9100            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9101            "get_weather"
9102        );
9103        assert_eq!(
9104            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9105            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
9106        );
9107    }
9108
9109    fn xml_object_argument_tool_request(stream: bool) -> Value {
9110        json!({
9111            "model": "stub-model",
9112            "messages": [{"role": "user", "content": "Weather in Berlin with a forecast."}],
9113            "stream": stream,
9114            "tools": [{
9115                "type": "function",
9116                "function": {
9117                    "name": "get_weather",
9118                    "parameters": {
9119                        "type": "object",
9120                        "$defs": {
9121                            "WeatherOptions": {
9122                                "type": "object",
9123                                "properties": {
9124                                    "unit": {"type": "string"},
9125                                    "include_forecast": {"type": "boolean"}
9126                                },
9127                                "required": ["unit", "include_forecast"],
9128                                "additionalProperties": false
9129                            }
9130                        },
9131                        "properties": {
9132                            "city": {"type": "string"},
9133                            "options": {"$ref": "#/$defs/WeatherOptions"}
9134                        },
9135                        "required": ["city", "options"],
9136                        "additionalProperties": false
9137                    }
9138                }
9139            }]
9140        })
9141    }
9142
9143    #[tokio::test]
9144    async fn route_chat_decodes_xml_object_argument_through_local_schema_ref() {
9145        let template = ModelChatTemplate::new(
9146            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
9147            "function-parameter-xml-template",
9148        );
9149        let response = post_json(
9150            router_with_stub_and_template(
9151                concat!(
9152                    "<tool_call>\n",
9153                    "<function=get_weather>\n",
9154                    "<parameter=city>\nBerlin\n</parameter>\n",
9155                    "<parameter=options>\n",
9156                    "{\"unit\":\"celsius\",\"include_forecast\":true}\n",
9157                    "</parameter>\n",
9158                    "</function>\n",
9159                    "</tool_call>",
9160                ),
9161                template,
9162            ),
9163            "/v1/chat/completions",
9164            xml_object_argument_tool_request(false),
9165        )
9166        .await;
9167
9168        assert_eq!(response.status(), AxumStatusCode::OK);
9169        let body = response_json(response).await;
9170        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9171        let arguments = body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
9172            .as_str()
9173            .and_then(|arguments| serde_json::from_str::<Value>(arguments).ok())
9174            .expect("tool arguments must contain one-decode structured JSON");
9175        assert_eq!(arguments["city"], json!("Berlin"));
9176        assert_eq!(
9177            arguments["options"],
9178            json!({"unit": "celsius", "include_forecast": true})
9179        );
9180    }
9181
9182    #[tokio::test]
9183    async fn route_chat_rejects_malformed_native_xml_object_argument() {
9184        let template = ModelChatTemplate::new(
9185            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
9186            "function-parameter-xml-template",
9187        );
9188        let mut request = xml_object_argument_tool_request(false);
9189        request["tools"][0]["function"]["strict"] = json!(true);
9190        let response = post_json(
9191            router_with_stub_and_template(
9192                concat!(
9193                    "<tool_call><function=get_weather>",
9194                    "<parameter=city>Berlin</parameter>",
9195                    "<parameter=options>{\"unit\":\"celsius\",</parameter>",
9196                    "</function></tool_call>",
9197                ),
9198                template,
9199            ),
9200            "/v1/chat/completions",
9201            request,
9202        )
9203        .await;
9204
9205        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
9206        let body = response_json(response).await;
9207        assert_eq!(body["error"]["type"], "internal_server_error");
9208        assert!(
9209            body["error"]["message"]
9210                .as_str()
9211                .is_some_and(|message| message.contains("did not satisfy its schema")),
9212            "body: {body}"
9213        );
9214    }
9215
9216    #[tokio::test]
9217    async fn route_streaming_chat_rejects_malformed_native_xml_before_tool_delta() {
9218        let template = ModelChatTemplate::new(
9219            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
9220            "function-parameter-xml-template",
9221        );
9222        let mut request = xml_object_argument_tool_request(true);
9223        request["tools"][0]["function"]["strict"] = json!(true);
9224        let response = post_json(
9225            router_with_stub_and_template(
9226                concat!(
9227                    "<tool_call><function=get_weather>",
9228                    "<parameter=city>Berlin</parameter>",
9229                    "<parameter=options>{\"unit\":\"celsius\",</parameter>",
9230                    "</function></tool_call>",
9231                ),
9232                template,
9233            ),
9234            "/v1/chat/completions",
9235            request,
9236        )
9237        .await;
9238
9239        assert_eq!(response.status(), AxumStatusCode::OK);
9240        let body = response_text(response).await;
9241        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9242        assert!(
9243            body.contains(r#""error":{"#) && body.contains("did not satisfy its schema"),
9244            "stream must return a controlled schema error: {body}"
9245        );
9246        assert!(
9247            !body.contains(r#""tool_calls":[{"#),
9248            "invalid native arguments must not leak a tool delta: {body}"
9249        );
9250    }
9251
9252    #[tokio::test]
9253    async fn route_chat_parses_tool_call_from_reasoning_before_fake_tool_result_content() {
9254        let response = post_json(
9255            router_with_stub(
9256                "kaza\n\
9257                 {\"name\":\"get_weather\",\"arguments\":{\"city\":\"北京\",\"unit\":\"celsius\"}}\n\
9258                 </think>\n\
9259                 {\"name\":\"get_weather\",\"content\":{\"temperature\":25,\"condition\":\"晴\"}}\n\
9260                 {\"temperature\":25,\"condition\":\"晴\"}",
9261            ),
9262            "/v1/chat/completions",
9263            json!({
9264                "model": "stub-model",
9265                "messages": [{"role": "user", "content": "北京现在天气怎么样?请先调用工具。"}],
9266                "tools": [{
9267                    "type": "function",
9268                    "function": {
9269                        "name": "get_weather",
9270                        "parameters": {
9271                            "type": "object",
9272                            "properties": {
9273                                "city": {"type": "string"},
9274                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
9275                            },
9276                            "required": ["city"]
9277                        }
9278                    }
9279                }]
9280            }),
9281        )
9282        .await;
9283        assert_eq!(response.status(), AxumStatusCode::OK);
9284        let body = response_json(response).await;
9285        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9286        assert_eq!(body["choices"][0]["message"]["content"], "");
9287        assert_eq!(
9288            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9289            "get_weather"
9290        );
9291        assert_eq!(
9292            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9293            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
9294        );
9295    }
9296
9297    #[tokio::test]
9298    async fn route_chat_prefers_reasoning_tool_call_over_empty_visible_arguments() {
9299        let response = post_json(
9300            router_with_stub(
9301                "{\"name\":\"get_weather\",\"arguments\":{\"city\":\"北京\",\"unit\":\"celsius\"}}\n\
9302                 </think>\n\
9303                 {\"name\":\"get_weather\",\"arguments\":{}}",
9304            ),
9305            "/v1/chat/completions",
9306            json!({
9307                "model": "stub-model",
9308                "messages": [{"role": "user", "content": "北京现在天气怎么样?请先调用 get_weather 工具。"}],
9309                "tools": [{
9310                    "type": "function",
9311                    "function": {
9312                        "name": "get_weather",
9313                        "parameters": {
9314                            "type": "object",
9315                            "properties": {
9316                                "city": {"type": "string"},
9317                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
9318                            },
9319                            "required": ["city"]
9320                        }
9321                    }
9322                }]
9323            }),
9324        )
9325        .await;
9326        assert_eq!(response.status(), AxumStatusCode::OK);
9327        let body = response_json(response).await;
9328        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9329        assert_eq!(
9330            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9331            "get_weather"
9332        );
9333        assert_eq!(
9334            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9335            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
9336        );
9337    }
9338
9339    #[tokio::test]
9340    async fn route_chat_honors_specific_tool_choice_for_generated_tool_call_json() {
9341        let response = post_json(
9342            router_with_stub(r#"{"name":"weather","arguments":{"city":"Paris"}}"#),
9343            "/v1/chat/completions",
9344            json!({
9345                "model": "stub-model",
9346                "messages": [{"role": "user", "content": "Use the selected tool."}],
9347                "tools": [
9348                    {
9349                        "type": "function",
9350                        "function": {"name": "weather", "parameters": {"type": "object"}}
9351                    },
9352                    {
9353                        "type": "function",
9354                        "function": {"name": "calendar", "parameters": {"type": "object"}}
9355                    }
9356                ],
9357                "tool_choice": {
9358                    "type": "function",
9359                    "function": {"name": "weather"}
9360                }
9361            }),
9362        )
9363        .await;
9364        assert_eq!(response.status(), AxumStatusCode::OK);
9365        let body = response_json(response).await;
9366        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9367        assert_eq!(
9368            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9369            "weather"
9370        );
9371
9372        let response = post_json(
9373            router_with_stub(r#"{"name":"calendar","arguments":{}}"#),
9374            "/v1/chat/completions",
9375            json!({
9376                "model": "stub-model",
9377                "messages": [{"role": "user", "content": "Use the selected tool."}],
9378                "tools": [
9379                    {
9380                        "type": "function",
9381                        "function": {"name": "weather", "parameters": {"type": "object"}}
9382                    },
9383                    {
9384                        "type": "function",
9385                        "function": {"name": "calendar", "parameters": {"type": "object"}}
9386                    }
9387                ],
9388                "tool_choice": {
9389                    "type": "function",
9390                    "function": {"name": "weather"}
9391                }
9392            }),
9393        )
9394        .await;
9395        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
9396        let body = response_json(response).await;
9397        assert_eq!(body["error"]["param"], "tool_choice");
9398        assert_eq!(body["error"]["type"], "invalid_request_error");
9399    }
9400
9401    #[tokio::test]
9402    async fn route_chat_specific_tool_choice_wraps_generated_arguments() {
9403        let response = post_json(
9404            router_with_stub(r#"{"city":"Paris"}"#),
9405            "/v1/chat/completions",
9406            json!({
9407                "model": "stub-model",
9408                "messages": [{"role": "user", "content": "Use the selected tool."}],
9409                "tools": [{
9410                    "type": "function",
9411                    "function": {
9412                        "name": "weather",
9413                        "parameters": {
9414                            "type": "object",
9415                            "properties": {"city": {"type": "string"}},
9416                            "required": ["city"]
9417                        }
9418                    }
9419                }],
9420                "tool_choice": {
9421                    "type": "function",
9422                    "function": {"name": "weather"}
9423                }
9424            }),
9425        )
9426        .await;
9427        assert_eq!(response.status(), AxumStatusCode::OK);
9428        let body = response_json(response).await;
9429        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9430        assert_eq!(body["choices"][0]["message"]["content"], "");
9431        assert_eq!(
9432            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9433            "weather"
9434        );
9435        assert_eq!(
9436            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9437            "{\"city\":\"Paris\"}"
9438        );
9439    }
9440
9441    #[tokio::test]
9442    async fn route_chat_tool_choice_none_keeps_generated_tool_json_as_content() {
9443        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
9444        let response = post_json(
9445            router_with_stub(generated),
9446            "/v1/chat/completions",
9447            json!({
9448                "model": "stub-model",
9449                "messages": [{"role": "user", "content": "Do not use tools."}],
9450                "tools": [{
9451                    "type": "function",
9452                    "function": {"name": "weather", "parameters": {"type": "object"}}
9453                }],
9454                "tool_choice": "none"
9455            }),
9456        )
9457        .await;
9458        assert_eq!(response.status(), AxumStatusCode::OK);
9459        let body = response_json(response).await;
9460        assert_eq!(body["choices"][0]["finish_reason"], "stop");
9461        assert_eq!(body["choices"][0]["message"]["content"], generated);
9462        assert!(body["choices"][0]["message"]["tool_calls"].is_null());
9463    }
9464
9465    #[tokio::test]
9466    async fn route_chat_tool_choice_required_wraps_generated_arguments() {
9467        let response = post_json(
9468            router_with_stub(r#"{"city":"Paris"}"#),
9469            "/v1/chat/completions",
9470            json!({
9471                "model": "stub-model",
9472                "messages": [{"role": "user", "content": "Use a tool."}],
9473                "tools": [{
9474                    "type": "function",
9475                    "function": {
9476                        "name": "weather",
9477                        "parameters": {
9478                            "type": "object",
9479                            "properties": {"city": {"type": "string"}},
9480                            "required": ["city"]
9481                        }
9482                    }
9483                }],
9484                "tool_choice": "required"
9485            }),
9486        )
9487        .await;
9488        assert_eq!(response.status(), AxumStatusCode::OK);
9489        let body = response_json(response).await;
9490        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9491        assert_eq!(body["choices"][0]["message"]["content"], "");
9492        assert_eq!(
9493            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9494            "weather"
9495        );
9496        assert_eq!(
9497            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9498            "{\"city\":\"Paris\"}"
9499        );
9500    }
9501
9502    fn required_tool_with_strict_response_format_request(stream: bool) -> Value {
9503        json!({
9504            "model": "stub-model",
9505            "messages": [{"role": "user", "content": "Use the weather tool."}],
9506            "stream": stream,
9507            "stream_options": stream.then_some(json!({"include_usage": true})),
9508            "tools": [{
9509                "type": "function",
9510                "function": {
9511                    "name": "weather",
9512                    "parameters": {
9513                        "type": "object",
9514                        "properties": {"city": {"type": "string", "const": "Paris"}},
9515                        "required": ["city"],
9516                        "additionalProperties": false
9517                    }
9518                }
9519            }],
9520            "tool_choice": "required",
9521            "response_format": {
9522                "type": "json_schema",
9523                "json_schema": {
9524                    "name": "content_answer",
9525                    "strict": true,
9526                    "schema": {
9527                        "type": "object",
9528                        "properties": {"answer": {"type": "string", "const": "IGNORED"}},
9529                        "required": ["answer"],
9530                        "additionalProperties": false
9531                    }
9532                }
9533            }
9534        })
9535    }
9536
9537    #[tokio::test]
9538    async fn route_chat_required_tool_takes_priority_over_strict_response_format() {
9539        let response = post_json(
9540            router_with_stub(r#"{"city":"Paris"}"#),
9541            "/v1/chat/completions",
9542            required_tool_with_strict_response_format_request(false),
9543        )
9544        .await;
9545        assert_eq!(response.status(), AxumStatusCode::OK);
9546        let body = response_json(response).await;
9547        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9548        assert_eq!(body["choices"][0]["message"]["content"], "");
9549        assert_eq!(
9550            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9551            "weather"
9552        );
9553        assert_eq!(
9554            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9555            r#"{"city":"Paris"}"#
9556        );
9557    }
9558
9559    #[tokio::test]
9560    async fn route_chat_required_tool_rejects_arguments_that_violate_const_schema() {
9561        let response = post_json(
9562            router_with_stub(r#"{"city":"London"}"#),
9563            "/v1/chat/completions",
9564            required_tool_with_strict_response_format_request(false),
9565        )
9566        .await;
9567        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
9568        let body = response_json(response).await;
9569        assert_eq!(body["error"]["type"], "internal_server_error");
9570        assert!(
9571            body["error"]["message"]
9572                .as_str()
9573                .is_some_and(|message| message.contains("did not satisfy its schema")),
9574            "body: {body}"
9575        );
9576    }
9577
9578    #[tokio::test]
9579    async fn route_streaming_required_tool_takes_priority_over_strict_response_format() {
9580        let response = post_json(
9581            router_with_stub(r#"{"city":"Paris"}"#),
9582            "/v1/chat/completions",
9583            required_tool_with_strict_response_format_request(true),
9584        )
9585        .await;
9586        assert_eq!(response.status(), AxumStatusCode::OK);
9587        let body = response_text(response).await;
9588        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9589        assert!(
9590            body.contains(r#""finish_reason":"tool_calls""#),
9591            "tool priority must finish with tool_calls: {body}"
9592        );
9593        assert!(
9594            body.contains(r#""name":"weather""#)
9595                && body.contains(r#""arguments":"{\"city\":\"Paris\"}""#),
9596            "stream must carry the reconstructed tool call: {body}"
9597        );
9598        assert_eq!(
9599            body.matches(r#""usage":{"#).count(),
9600            1,
9601            "stream must carry exactly one usage row: {body}"
9602        );
9603        assert!(
9604            !body.contains("strict json_schema") && !body.contains("invalid JSON"),
9605            "dormant content schema must not reject a required tool call: {body}"
9606        );
9607    }
9608
9609    #[tokio::test]
9610    async fn dropping_buffered_http_response_drops_the_engine_stream() {
9611        let stream_dropped = Arc::new(Notify::new());
9612        let response = post_json(
9613            AxumServer::from_llm(Arc::new(StubLlm::with_pending_stream(Arc::clone(
9614                &stream_dropped,
9615            ))))
9616            .build_router(),
9617            "/v1/chat/completions",
9618            required_tool_with_strict_response_format_request(true),
9619        )
9620        .await;
9621        assert_eq!(response.status(), AxumStatusCode::OK);
9622
9623        drop(response);
9624        tokio::time::timeout(std::time::Duration::from_secs(1), stream_dropped.notified())
9625            .await
9626            .expect("client disconnect must stop a buffered structured stream promptly");
9627    }
9628
9629    #[tokio::test]
9630    async fn route_chat_tool_choice_required_errors_without_valid_tool_call() {
9631        let response = post_json(
9632            router_with_stub("plain answer"),
9633            "/v1/chat/completions",
9634            json!({
9635                "model": "stub-model",
9636                "messages": [{"role": "user", "content": "Use a tool."}],
9637                "tools": [{
9638                    "type": "function",
9639                    "function": {"name": "weather", "parameters": {"type": "object"}}
9640                }],
9641                "tool_choice": "required"
9642            }),
9643        )
9644        .await;
9645        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
9646        let body = response_json(response).await;
9647        assert_eq!(body["error"]["type"], "invalid_request_error");
9648        assert_eq!(body["error"]["param"], "tool_choice");
9649        assert!(
9650            body["error"]["message"]
9651                .as_str()
9652                .is_some_and(|message| message.contains("required tool_choice")),
9653            "body: {body}"
9654        );
9655    }
9656
9657    #[tokio::test]
9658    async fn route_streaming_chat_serializes_generated_tool_call_delta() {
9659        let response = post_json(
9660            router_with_stub(
9661                r#"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"weather","arguments":{"city":"Paris"}}}]}"#,
9662            ),
9663            "/v1/chat/completions",
9664            json!({
9665                "model": "stub-model",
9666                "messages": [{"role": "user", "content": "Use the weather tool."}],
9667                "stream": true,
9668                "tools": [{
9669                    "type": "function",
9670                    "function": {
9671                        "name": "weather",
9672                        "parameters": {
9673                            "type": "object",
9674                            "properties": {"city": {"type": "string"}},
9675                            "required": ["city"]
9676                        }
9677                    }
9678                }],
9679                "tool_choice": "auto"
9680            }),
9681        )
9682        .await;
9683        assert_eq!(response.status(), AxumStatusCode::OK);
9684        let body = response_text(response).await;
9685        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9686        assert!(
9687            body.contains(r#""finish_reason":"tool_calls""#),
9688            "stream should finish with tool_calls: {body}"
9689        );
9690        assert!(
9691            body.contains(r#""tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"weather""#),
9692            "stream should emit OpenAI tool_calls delta with index: {body}"
9693        );
9694        assert!(
9695            body.contains(r#""arguments":"{\"city\":\"Paris\"}""#),
9696            "tool arguments should be serialized as JSON string: {body}"
9697        );
9698        assert!(
9699            !body.contains(r#""content":"{\"tool_calls\""#),
9700            "raw tool-call JSON should not be streamed as assistant content: {body}"
9701        );
9702    }
9703
9704    #[tokio::test]
9705    async fn route_streaming_chat_serializes_qwen3_function_parameters_tool_delta() {
9706        let response = post_json(
9707            router_with_stub(
9708                r#"{"function":"get_weather","parameters":{"city":"深圳","unit":"c"}}"#,
9709            ),
9710            "/v1/chat/completions",
9711            json!({
9712                "model": "stub-model",
9713                "messages": [{"role": "user", "content": "深圳天气?"}],
9714                "stream": true,
9715                "tools": [{
9716                    "type": "function",
9717                    "function": {
9718                        "name": "get_weather",
9719                        "parameters": {
9720                            "type": "object",
9721                            "properties": {
9722                                "city": {"type": "string"},
9723                                "unit": {"type": "string", "enum": ["c", "f"]}
9724                            },
9725                            "required": ["city"]
9726                        }
9727                    }
9728                }],
9729                "tool_choice": "auto"
9730            }),
9731        )
9732        .await;
9733        assert_eq!(response.status(), AxumStatusCode::OK);
9734        let body = response_text(response).await;
9735        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9736        assert!(
9737            body.contains(r#""finish_reason":"tool_calls""#),
9738            "stream should finish with tool_calls: {body}"
9739        );
9740        assert!(
9741            body.contains(r#""function":{"name":"get_weather","arguments":"{\"city\":\"深圳\",\"unit\":\"c\"}"}"#),
9742            "stream should emit parsed Qwen3 function parameters as tool args: {body}"
9743        );
9744        assert!(
9745            !body.contains(r#""content":"{\"function\""#),
9746            "raw Qwen3 tool JSON should not leak as assistant content: {body}"
9747        );
9748    }
9749
9750    #[tokio::test]
9751    async fn route_streaming_chat_preserves_opencode_edit_xml_whitespace() {
9752        let template = ModelChatTemplate::new(
9753            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
9754            "function-parameter-xml-template",
9755        );
9756        let generated = concat!(
9757            "<tool_call>\n",
9758            "<function=edit>\n",
9759            "<parameter=filePath>\n",
9760            "/workspace/src/main.rs\n",
9761            "</parameter>\n",
9762            "<parameter=oldString>\n",
9763            "    if x:\n",
9764            "        return 1\n",
9765            "\n",
9766            "</parameter>\n",
9767            "<parameter=newString>\n",
9768            "    if x:\n",
9769            "        return 2\n",
9770            "\n",
9771            "</parameter>\n",
9772            "<parameter=replaceAll>\n",
9773            "true\n",
9774            "</parameter>\n",
9775            "</function>\n",
9776            "</tool_call>",
9777        );
9778        let response = post_json(
9779            router_with_stub_and_template(generated, template),
9780            "/v1/chat/completions",
9781            json!({
9782                "model": "stub-model",
9783                "messages": [{"role": "user", "content": "Replace the code."}],
9784                "stream": true,
9785                "tools": [{
9786                    "type": "function",
9787                    "function": {
9788                        "name": "edit",
9789                        "parameters": {
9790                            "type": "object",
9791                            "properties": {
9792                                "filePath": {"type": "string"},
9793                                "oldString": {"type": "string"},
9794                                "newString": {"type": "string"},
9795                                "replaceAll": {"type": "boolean"}
9796                            },
9797                            "required": ["filePath", "oldString", "newString"]
9798                        }
9799                    }
9800                }]
9801            }),
9802        )
9803        .await;
9804
9805        assert_eq!(response.status(), AxumStatusCode::OK);
9806        let body = response_text(response).await;
9807        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9808        assert!(
9809            body.contains(r#"\"oldString\":\"    if x:\\n        return 1\\n\""#),
9810            "stream must preserve exact code whitespace in tool arguments: {body}"
9811        );
9812        assert!(
9813            body.contains(r#"\"replaceAll\":true"#),
9814            "stream must preserve the boolean tool argument type: {body}"
9815        );
9816    }
9817
9818    #[tokio::test]
9819    async fn route_streaming_chat_honors_specific_tool_choice_for_generated_tool_call_delta() {
9820        let request = |generated: &'static str| {
9821            post_json(
9822                router_with_stub(generated),
9823                "/v1/chat/completions",
9824                json!({
9825                    "model": "stub-model",
9826                    "messages": [{"role": "user", "content": "Use the selected tool."}],
9827                    "stream": true,
9828                    "tools": [
9829                        {
9830                            "type": "function",
9831                            "function": {"name": "weather", "parameters": {"type": "object"}}
9832                        },
9833                        {
9834                            "type": "function",
9835                            "function": {"name": "calendar", "parameters": {"type": "object"}}
9836                        }
9837                    ],
9838                    "tool_choice": {
9839                        "type": "function",
9840                        "function": {"name": "weather"}
9841                    }
9842                }),
9843            )
9844        };
9845
9846        let response = request(r#"{"name":"weather","arguments":{"city":"Paris"}}"#).await;
9847        assert_eq!(response.status(), AxumStatusCode::OK);
9848        let body = response_text(response).await;
9849        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9850        assert!(
9851            body.contains(r#""finish_reason":"tool_calls""#),
9852            "selected tool should finish with tool_calls: {body}"
9853        );
9854        assert!(
9855            body.contains(r#""function":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#),
9856            "selected tool should stream as tool_calls delta: {body}"
9857        );
9858
9859        let response = request(r#"{"name":"calendar","arguments":{}}"#).await;
9860        assert_eq!(response.status(), AxumStatusCode::OK);
9861        let body = response_text(response).await;
9862        assert!(
9863            body.contains(
9864                r#""error":{"message":"model output did not satisfy required tool_choice""#
9865            ),
9866            "selected-tool stream should reject unselected tool output: {body}"
9867        );
9868        assert!(
9869            !body.contains(r#""finish_reason":"tool_calls""#),
9870            "unselected tool JSON must not become tool_calls: {body}"
9871        );
9872    }
9873
9874    #[tokio::test]
9875    async fn route_streaming_chat_prefers_chunk_api_response_for_tool_delta() {
9876        let response = post_json(
9877            router_with_stub_api_response(
9878                "raw text that should not stream",
9879                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
9880                    message: ferrum_types::ApiChatMessage {
9881                        role: ferrum_types::ApiMessageRole::Assistant,
9882                        content: String::new(),
9883                        name: None,
9884                        tool_calls: vec![ferrum_types::ApiToolCall {
9885                            id: "call_1".to_string(),
9886                            tool_type: "function".to_string(),
9887                            function: ferrum_types::ApiFunctionCall {
9888                                name: "weather".to_string(),
9889                                arguments: "{\"city\":\"Paris\"}".to_string(),
9890                            },
9891                        }],
9892                        tool_call_id: None,
9893                        function_call: None,
9894                    },
9895                    finish_reason: Some("tool_calls".to_string()),
9896                }),
9897            ),
9898            "/v1/chat/completions",
9899            json!({
9900                "model": "stub-model",
9901                "messages": [{"role": "user", "content": "Use the weather tool."}],
9902                "stream": true,
9903                "tools": [{
9904                    "type": "function",
9905                    "function": {"name": "weather", "parameters": {"type": "object"}}
9906                }],
9907                "tool_choice": "auto"
9908            }),
9909        )
9910        .await;
9911        assert_eq!(response.status(), AxumStatusCode::OK);
9912        let body = response_text(response).await;
9913        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9914        assert!(
9915            body.contains(r#""finish_reason":"tool_calls""#),
9916            "stream should finish with tool_calls: {body}"
9917        );
9918        assert!(
9919            body.contains(r#""tool_calls":[{"index":0,"id":"call_1""#),
9920            "stream should emit tool_calls from chunk api_response: {body}"
9921        );
9922        assert!(
9923            !body.contains("raw text that should not stream"),
9924            "structured api_response should suppress raw generated text in tool-call stream: {body}"
9925        );
9926    }
9927
9928    #[tokio::test]
9929    async fn route_streaming_chat_preserves_length_over_structured_tool_response() {
9930        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
9931        let response = post_json(
9932            router_with_stub_api_response_and_finish_reason(
9933                generated,
9934                weather_tool_api_response(),
9935                FinishReason::Length,
9936            ),
9937            "/v1/chat/completions",
9938            json!({
9939                "model": "stub-model",
9940                "messages": [{"role": "user", "content": "Use the weather tool."}],
9941                "stream": true,
9942                "tools": [{
9943                    "type": "function",
9944                    "function": {"name": "weather", "parameters": {"type": "object"}}
9945                }],
9946                "tool_choice": "auto"
9947            }),
9948        )
9949        .await;
9950        assert_eq!(response.status(), AxumStatusCode::OK);
9951        let body = response_text(response).await;
9952        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9953        assert!(
9954            body.contains(r#""finish_reason":"length""#),
9955            "stream must preserve the engine terminal reason: {body}"
9956        );
9957        assert!(
9958            !body.contains(r#""finish_reason":"tool_calls""#),
9959            "length must not be relabeled as tool_calls: {body}"
9960        );
9961    }
9962
9963    #[tokio::test]
9964    async fn route_streaming_chat_tool_choice_required_errors_without_leaking_content() {
9965        let response = post_json(
9966            router_with_stub("plain answer"),
9967            "/v1/chat/completions",
9968            json!({
9969                "model": "stub-model",
9970                "messages": [{"role": "user", "content": "Use a tool."}],
9971                "stream": true,
9972                "tools": [{
9973                    "type": "function",
9974                    "function": {"name": "weather", "parameters": {"type": "object"}}
9975                }],
9976                "tool_choice": "required"
9977            }),
9978        )
9979        .await;
9980        assert_eq!(response.status(), AxumStatusCode::OK);
9981        let body = response_text(response).await;
9982        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9983        assert!(
9984            body.contains(
9985                r#""error":{"message":"model output did not satisfy required tool_choice""#
9986            ),
9987            "stream should emit OpenAI error envelope: {body}"
9988        );
9989        assert!(
9990            body.contains(r#""type":"invalid_request_error""#),
9991            "stream should use invalid_request_error: {body}"
9992        );
9993        assert!(
9994            body.contains(r#""param":"tool_choice""#),
9995            "stream should include tool_choice param: {body}"
9996        );
9997        assert!(
9998            !body.contains(r#""content":"plain answer""#),
9999            "required stream must not leak invalid content before validation: {body}"
10000        );
10001    }
10002
10003    #[tokio::test]
10004    async fn route_streaming_chat_tool_request_falls_back_to_content_when_no_tool_call() {
10005        let response = post_json(
10006            router_with_stub("plain answer"),
10007            "/v1/chat/completions",
10008            json!({
10009                "model": "stub-model",
10010                "messages": [{"role": "user", "content": "Use the weather tool if needed."}],
10011                "stream": true,
10012                "tools": [{
10013                    "type": "function",
10014                    "function": {"name": "weather", "parameters": {"type": "object"}}
10015                }],
10016                "tool_choice": "auto"
10017            }),
10018        )
10019        .await;
10020        assert_eq!(response.status(), AxumStatusCode::OK);
10021        let body = response_text(response).await;
10022        assert!(
10023            body.contains(r#""content":"plain answer""#),
10024            "plain content should still stream when no tool call is generated: {body}"
10025        );
10026        assert!(
10027            body.contains(r#""finish_reason":"stop""#),
10028            "plain content should keep normal finish reason: {body}"
10029        );
10030        assert!(
10031            !body.contains(r#""tool_calls""#),
10032            "fallback content should not synthesize tool_calls: {body}"
10033        );
10034    }
10035
10036    #[tokio::test]
10037    async fn route_streaming_chat_serializes_generated_legacy_function_call_delta() {
10038        let response = post_json(
10039            router_with_stub(
10040                r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#,
10041            ),
10042            "/v1/chat/completions",
10043            json!({
10044                "model": "stub-model",
10045                "messages": [{"role": "user", "content": "Use the weather function."}],
10046                "stream": true,
10047                "functions": [{
10048                    "name": "weather",
10049                    "parameters": {
10050                        "type": "object",
10051                        "properties": {"city": {"type": "string"}},
10052                        "required": ["city"]
10053                    }
10054                }],
10055                "function_call": "auto"
10056            }),
10057        )
10058        .await;
10059        assert_eq!(response.status(), AxumStatusCode::OK);
10060        let body = response_text(response).await;
10061        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
10062        assert!(
10063            body.contains(r#""finish_reason":"function_call""#),
10064            "stream should finish with function_call: {body}"
10065        );
10066        assert!(
10067            body.contains(
10068                r#""function_call":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#
10069            ),
10070            "stream should emit OpenAI legacy function_call delta: {body}"
10071        );
10072        assert!(
10073            !body.contains(r#""content":"{\"function_call\""#),
10074            "raw function-call JSON should not be streamed as assistant content: {body}"
10075        );
10076    }
10077
10078    #[tokio::test]
10079    async fn route_streaming_chat_honors_specific_legacy_function_call_delta() {
10080        let request = |generated: &'static str| {
10081            post_json(
10082                router_with_stub(generated),
10083                "/v1/chat/completions",
10084                json!({
10085                    "model": "stub-model",
10086                    "messages": [{"role": "user", "content": "Use the selected function."}],
10087                    "stream": true,
10088                    "functions": [
10089                        {"name": "weather", "parameters": {"type": "object"}},
10090                        {"name": "calendar", "parameters": {"type": "object"}}
10091                    ],
10092                    "function_call": {"name": "weather"}
10093                }),
10094            )
10095        };
10096
10097        let response =
10098            request(r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#).await;
10099        assert_eq!(response.status(), AxumStatusCode::OK);
10100        let body = response_text(response).await;
10101        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
10102        assert!(
10103            body.contains(r#""finish_reason":"function_call""#),
10104            "selected function should finish with function_call: {body}"
10105        );
10106        assert!(
10107            body.contains(
10108                r#""function_call":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#
10109            ),
10110            "selected function should stream as function_call delta: {body}"
10111        );
10112
10113        let response = request(r#"{"function_call":{"name":"calendar","arguments":{}}}"#).await;
10114        assert_eq!(response.status(), AxumStatusCode::OK);
10115        let body = response_text(response).await;
10116        assert!(
10117            body.contains(
10118                r#""content":"{\"function_call\":{\"name\":\"calendar\",\"arguments\":{}}}""#
10119            ),
10120            "unselected function JSON should stream as ordinary content: {body}"
10121        );
10122        assert!(
10123            body.contains(r#""finish_reason":"stop""#),
10124            "unselected function JSON should keep normal stop finish: {body}"
10125        );
10126        assert!(
10127            !body.contains(r#""finish_reason":"function_call""#),
10128            "unselected function JSON must not become function_call: {body}"
10129        );
10130    }
10131
10132    #[tokio::test]
10133    async fn route_chat_serializes_generated_legacy_function_call_when_engine_returns_text_only() {
10134        let response = post_json(
10135            router_with_stub(
10136                r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#,
10137            ),
10138            "/v1/chat/completions",
10139            json!({
10140                "model": "stub-model",
10141                "messages": [{"role": "user", "content": "Use the weather function."}],
10142                "functions": [{
10143                    "name": "weather",
10144                    "parameters": {
10145                        "type": "object",
10146                        "properties": {"city": {"type": "string"}},
10147                        "required": ["city"]
10148                    }
10149                }],
10150                "function_call": "auto"
10151            }),
10152        )
10153        .await;
10154        assert_eq!(response.status(), AxumStatusCode::OK);
10155        let body = response_json(response).await;
10156        assert_eq!(body["choices"][0]["finish_reason"], "function_call");
10157        assert_eq!(body["choices"][0]["message"]["content"], "");
10158        assert_eq!(
10159            body["choices"][0]["message"]["function_call"]["name"],
10160            "weather"
10161        );
10162        assert_eq!(
10163            body["choices"][0]["message"]["function_call"]["arguments"],
10164            "{\"city\":\"Paris\"}"
10165        );
10166    }
10167
10168    #[tokio::test]
10169    async fn route_chat_serializes_legacy_function_call_response() {
10170        let response = post_json(
10171            router_with_stub_api_response(
10172                "",
10173                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
10174                    message: ferrum_types::ApiChatMessage {
10175                        role: ferrum_types::ApiMessageRole::Assistant,
10176                        content: String::new(),
10177                        name: None,
10178                        tool_calls: vec![],
10179                        tool_call_id: None,
10180                        function_call: Some(ferrum_types::ApiFunctionCall {
10181                            name: "weather".to_string(),
10182                            arguments: "{\"city\":\"Paris\"}".to_string(),
10183                        }),
10184                    },
10185                    finish_reason: Some("function_call".to_string()),
10186                }),
10187            ),
10188            "/v1/chat/completions",
10189            json!({
10190                "model": "stub-model",
10191                "messages": [{"role": "user", "content": "Use the weather function."}],
10192                "functions": [{
10193                    "name": "weather",
10194                    "parameters": {
10195                        "type": "object",
10196                        "properties": {"city": {"type": "string"}},
10197                        "required": ["city"]
10198                    }
10199                }],
10200                "function_call": "auto"
10201            }),
10202        )
10203        .await;
10204        assert_eq!(response.status(), AxumStatusCode::OK);
10205        let body = response_json(response).await;
10206        assert_eq!(body["choices"][0]["finish_reason"], "function_call");
10207        assert_eq!(
10208            body["choices"][0]["message"]["function_call"]["name"],
10209            "weather"
10210        );
10211        assert_eq!(
10212            body["choices"][0]["message"]["function_call"]["arguments"],
10213            "{\"city\":\"Paris\"}"
10214        );
10215    }
10216
10217    #[tokio::test]
10218    async fn route_streaming_chat_include_usage_contract() {
10219        let response = post_json(
10220            router_with_stub("ok"),
10221            "/v1/chat/completions",
10222            json!({
10223                "model": "stub-model",
10224                "messages": [{"role": "user", "content": "Say ok"}],
10225                "stream": true,
10226                "stream_options": {"include_usage": true}
10227            }),
10228        )
10229        .await;
10230        assert_eq!(response.status(), AxumStatusCode::OK);
10231        let body = response_text(response).await;
10232        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
10233        assert!(
10234            body.contains("\"object\":\"chat.completion.chunk\""),
10235            "missing chat chunk: {body}"
10236        );
10237        assert!(
10238            body.contains("\"usage\":{\"prompt_tokens\""),
10239            "missing final usage chunk: {body}"
10240        );
10241        assert!(
10242            body.contains("\"choices\":[],\"usage\""),
10243            "usage should be emitted as a separate chunk: {body}"
10244        );
10245        assert!(
10246            body.contains("\"prompt_tokens\":5"),
10247            "stream usage should come from engine token usage: {body}"
10248        );
10249    }
10250
10251    #[tokio::test]
10252    async fn route_streaming_chat_waits_for_separate_final_usage_at_max_tokens() {
10253        let response = post_json(
10254            router_with_stub_separate_final_stream_chunk(&["he", "llo"]),
10255            "/v1/chat/completions",
10256            json!({
10257                "model": "stub-model",
10258                "messages": [{"role": "user", "content": "Say hello"}],
10259                "max_tokens": 2,
10260                "stream": true,
10261                "stream_options": {"include_usage": true}
10262            }),
10263        )
10264        .await;
10265        assert_eq!(response.status(), AxumStatusCode::OK);
10266        let body = response_text(response).await;
10267        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
10268        assert!(
10269            body.contains("\"content\":\"he\""),
10270            "missing first chunk: {body}"
10271        );
10272        assert!(
10273            body.contains("\"content\":\"llo\""),
10274            "missing second chunk: {body}"
10275        );
10276        assert!(
10277            body.contains("\"choices\":[],\"usage\""),
10278            "missing separate usage chunk from final engine chunk: {body}"
10279        );
10280        assert!(
10281            body.contains("\"prompt_tokens\":5"),
10282            "stream usage should come from engine final usage: {body}"
10283        );
10284    }
10285
10286    #[tokio::test]
10287    async fn route_streaming_preserves_tokenless_tail_before_terminal() {
10288        for (path, chunks, expected_content, expected_reasoning) in [
10289            ("/v1/chat/completions", ["hello ", "尾"], "hello 尾", ""),
10290            (
10291                "/v1/chat/completions",
10292                ["<think>reason", "</think>"],
10293                "",
10294                "reason",
10295            ),
10296            ("/v1/completions", ["hello ", "尾"], "hello 尾", ""),
10297        ] {
10298            let chat = path == "/v1/chat/completions";
10299            let mut request = json!({"model": "stub-model", "stream": true});
10300            if chat {
10301                request["messages"] = json!([{"role": "user", "content": "hello"}]);
10302                request["stream_options"] = json!({"include_usage": true});
10303            } else {
10304                request["prompt"] = json!("hello");
10305            }
10306            let router = AxumServer::from_llm(Arc::new(StubLlm::with_tokenless_tail(&chunks)))
10307                .build_router();
10308            let response = post_json(router, path, request).await;
10309            assert_eq!(response.status(), AxumStatusCode::OK);
10310            let body = response_text(response).await;
10311            let events = responses_sse_json_events(&body);
10312            let content: String = events
10313                .iter()
10314                .filter_map(|event| {
10315                    if chat {
10316                        event["choices"][0]["delta"]["content"].as_str()
10317                    } else {
10318                        event["choices"][0]["text"].as_str()
10319                    }
10320                })
10321                .collect();
10322            let reasoning: String = events
10323                .iter()
10324                .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
10325                .collect();
10326            assert_eq!(content, expected_content, "body: {body}");
10327            assert_eq!(reasoning, expected_reasoning, "body: {body}");
10328            assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
10329            assert_eq!(
10330                events
10331                    .iter()
10332                    .filter(|event| event["choices"][0]["finish_reason"] == "stop")
10333                    .count(),
10334                1,
10335                "body: {body}"
10336            );
10337            let usage: Vec<_> = events
10338                .iter()
10339                .filter_map(|event| event["usage"].as_object())
10340                .collect();
10341            assert_eq!(usage.len(), 1, "body: {body}");
10342            assert_eq!(usage[0]["prompt_tokens"], 5);
10343            assert_eq!(usage[0]["completion_tokens"], 2);
10344        }
10345    }
10346
10347    #[tokio::test]
10348    async fn route_rejects_multimodal_content_with_400() {
10349        for content in [
10350            json!([{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}]),
10351            json!([{"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}}]),
10352            json!([{"type": "video_url", "video_url": {"url": "https://example.com/video.mp4"}}]),
10353            json!([
10354                {"type": "text", "text": "describe this"},
10355                {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}
10356            ]),
10357        ] {
10358            let response = post_json(
10359                router_with_stub("unused"),
10360                "/v1/chat/completions",
10361                json!({
10362                    "model": "stub-model",
10363                    "messages": [{"role": "user", "content": content}]
10364                }),
10365            )
10366            .await;
10367            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10368            let body = response_json(response).await;
10369            assert_eq!(body["error"]["type"], "invalid_request_error");
10370            let message = body["error"]["message"].as_str().unwrap();
10371            assert!(message.contains("invalid chat completions request"));
10372            assert!(
10373                message.contains("unsupported message content part type"),
10374                "body: {body}"
10375            );
10376        }
10377    }
10378
10379    #[tokio::test]
10380    async fn route_rejects_non_object_stream_options() {
10381        for stream_options in [json!([]), json!("yes"), json!(42), json!(true)] {
10382            let response = post_json(
10383                router_with_stub("unused"),
10384                "/v1/chat/completions",
10385                json!({
10386                    "model": "stub-model",
10387                    "messages": [{"role": "user", "content": "hello"}],
10388                    "stream": true,
10389                    "stream_options": stream_options
10390                }),
10391            )
10392            .await;
10393            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10394            let body = response_json(response).await;
10395            assert_eq!(body["error"]["type"], "invalid_request_error");
10396            assert!(
10397                body["error"]["message"]
10398                    .as_str()
10399                    .unwrap_or_default()
10400                    .contains("stream_options must be a JSON object"),
10401                "body: {body}"
10402            );
10403        }
10404    }
10405
10406    #[tokio::test]
10407    async fn route_accepts_text_only_content_array() {
10408        let response = post_json(
10409            router_with_stub("ok"),
10410            "/v1/chat/completions",
10411            json!({
10412                "model": "stub-model",
10413                "messages": [{
10414                    "role": "user",
10415                    "content": [
10416                        {"type": "text", "text": "say"},
10417                        {"type": "text", "text": "ok"}
10418                    ]
10419                }]
10420            }),
10421        )
10422        .await;
10423        assert_eq!(response.status(), AxumStatusCode::OK);
10424        let body = response_json(response).await;
10425        assert_eq!(body["choices"][0]["message"]["content"], "ok");
10426    }
10427
10428    #[tokio::test]
10429    async fn route_chat_invalid_json_maps_to_openai_error() {
10430        let response = post_raw_json(router_with_stub("unused"), "/v1/chat/completions", "{").await;
10431        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10432        let body = response_json(response).await;
10433        assert_eq!(body["error"]["type"], "invalid_request_error");
10434        assert_eq!(body["error"]["param"], Value::Null);
10435        assert!(body["error"]["message"]
10436            .as_str()
10437            .unwrap()
10438            .contains("invalid chat completions request"));
10439    }
10440
10441    #[tokio::test]
10442    async fn route_rejects_logit_bias_with_openai_error_param() {
10443        let response = post_json(
10444            router_with_stub("unused"),
10445            "/v1/chat/completions",
10446            json!({
10447                "model": "stub-model",
10448                "messages": [{"role": "user", "content": "hello"}],
10449                "logit_bias": {"1": 42.0}
10450            }),
10451        )
10452        .await;
10453        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10454        let body = response_json(response).await;
10455        assert_eq!(body["error"]["type"], "invalid_request_error");
10456        assert_eq!(body["error"]["param"], "logit_bias");
10457    }
10458
10459    #[tokio::test]
10460    async fn route_tool_request_reaches_engine_structured_boundary() {
10461        for stream in [false, true] {
10462            let (router, engine) = router_with_capturing_llm();
10463            let response = post_json(
10464                router,
10465                "/v1/chat/completions",
10466                json!({
10467                    "model": "qwen3",
10468                    "messages": [
10469                        {"role": "user", "content": "Use the weather tool."},
10470                        {
10471                            "role": "assistant",
10472                            "content": null,
10473                            "tool_calls": [{
10474                                "id": "call_1",
10475                                "type": "function",
10476                                "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
10477                            }]
10478                        },
10479                        {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
10480                    ],
10481                    "tools": [{
10482                        "type": "function",
10483                        "function": {
10484                            "name": "weather",
10485                            "description": "Get weather",
10486                            "parameters": {
10487                                "type": "object",
10488                                "properties": {"city": {"type": "string"}},
10489                                "required": ["city"]
10490                            }
10491                        }
10492                    }],
10493                    "tool_choice": "auto",
10494                    "functions": [{
10495                        "name": "legacy_weather",
10496                        "parameters": {"type": "object", "properties": {}}
10497                    }],
10498                    "function_call": "auto",
10499                    "stream": stream
10500                }),
10501            )
10502            .await;
10503            assert_eq!(response.status(), AxumStatusCode::OK);
10504
10505            if stream {
10506                let body = response_text(response).await;
10507                assert!(body.contains("[DONE]"), "{body}");
10508                assert!(body.contains("captured"), "{body}");
10509            } else {
10510                let body = response_json(response).await;
10511                assert_eq!(body["choices"][0]["message"]["content"], "captured");
10512                assert_eq!(body["choices"][0]["finish_reason"], "stop");
10513            }
10514            let request = engine.last_request();
10515            assert!(request.prompt.contains("\"tools\":[{"));
10516            assert!(request.prompt.contains("\"type\":\"function\""));
10517            assert!(request.prompt.contains("\"name\":\"weather\""));
10518            assert!(request.prompt.contains("<|im_start|>assistant\n{"));
10519            assert!(request.prompt.contains("\"tool_calls\":[{"));
10520            assert!(request.prompt.contains("\"id\":\"call_1\""));
10521            assert!(request.prompt.contains("<|im_start|>tool\nsunny<|im_end|>"));
10522            assert_eq!(
10523                request.metadata["openai_tools"][0]["function"]["name"],
10524                "weather"
10525            );
10526            assert_eq!(request.metadata["openai_tool_choice"], "auto");
10527            assert_eq!(
10528                request.metadata["openai_legacy_functions"][0]["name"],
10529                "legacy_weather"
10530            );
10531            assert_eq!(request.metadata["openai_legacy_function_call"], "auto");
10532            let Some(ferrum_types::ApiRequest::Chat(api)) = request.api_request.as_ref() else {
10533                panic!("expected structured chat api_request");
10534            };
10535            assert_eq!(api.messages.len(), 3);
10536            assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Tool);
10537            assert_eq!(api.messages[2].tool_call_id.as_deref(), Some("call_1"));
10538            assert_eq!(api.messages[1].tool_calls[0].id, "call_1");
10539            assert_eq!(api.messages[1].tool_calls[0].function.name, "weather");
10540            assert_eq!(api.messages[2].content, "sunny");
10541            assert_eq!(api.tools[0].function.name, "weather");
10542            assert_eq!(api.legacy_functions[0].name, "legacy_weather");
10543            assert_eq!(
10544                api.messages[1].tool_calls[0].function.arguments,
10545                "{\"city\":\"Paris\"}"
10546            );
10547        }
10548    }
10549
10550    #[tokio::test]
10551    async fn route_replays_reasoning_content_in_qwen36_tool_history_sync() {
10552        let compatibility = capture_qwen36_tool_history_request(
10553            json!({"reasoning_content": "opencode-reasoning-marker"}),
10554            false,
10555        )
10556        .await;
10557        let canonical = capture_qwen36_tool_history_request(
10558            json!({"reasoning": "opencode-reasoning-marker"}),
10559            false,
10560        )
10561        .await;
10562
10563        assert_eq!(compatibility.prompt, canonical.prompt);
10564        assert!(
10565            compatibility.prompt.contains("opencode-reasoning-marker"),
10566            "Qwen3.6 prompt dropped assistant reasoning history: {}",
10567            compatibility.prompt
10568        );
10569        let message = &compatibility.metadata["openai_messages"][1];
10570        assert_eq!(message["reasoning"], "opencode-reasoning-marker");
10571        assert!(message.get("reasoning_content").is_none());
10572    }
10573
10574    #[tokio::test]
10575    async fn route_replays_reasoning_content_in_qwen36_tool_history_stream() {
10576        let request = capture_qwen36_tool_history_request(
10577            json!({"reasoning_content": "opencode-stream-reasoning-marker"}),
10578            true,
10579        )
10580        .await;
10581        assert!(
10582            request.prompt.contains("opencode-stream-reasoning-marker"),
10583            "Qwen3.6 streaming prompt dropped assistant reasoning history: {}",
10584            request.prompt
10585        );
10586    }
10587
10588    #[tokio::test]
10589    async fn route_prefers_canonical_reasoning_in_qwen36_tool_history() {
10590        for stream in [false, true] {
10591            for reasoning in ["canonical-history-marker", ""] {
10592                let request = capture_qwen36_tool_history_request(
10593                    json!({
10594                        "reasoning": reasoning,
10595                        "reasoning_content": "alias-history-marker"
10596                    }),
10597                    stream,
10598                )
10599                .await;
10600                let canonical =
10601                    capture_qwen36_tool_history_request(json!({"reasoning": reasoning}), stream)
10602                        .await;
10603                assert_eq!(request.prompt, canonical.prompt);
10604                assert!(!request.prompt.contains("alias-history-marker"));
10605                let message = &request.metadata["openai_messages"][1];
10606                assert_eq!(message["reasoning"], reasoning);
10607                assert!(message.get("reasoning_content").is_none());
10608            }
10609        }
10610    }
10611
10612    #[tokio::test]
10613    async fn route_does_not_force_reasoning_into_templates_that_ignore_it() {
10614        let template = ModelChatTemplate::new(
10615            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}",
10616            "content-only-template",
10617        );
10618        let (router, engine) = router_with_capturing_llm_and_template(template);
10619        let response = post_json(
10620            router,
10621            "/v1/chat/completions",
10622            json!({
10623                "model": "served-alias",
10624                "messages": [
10625                    {"role": "user", "content": "hello"},
10626                    {
10627                        "role": "assistant",
10628                        "content": "visible answer",
10629                        "reasoning_content": "hidden-reasoning-marker"
10630                    },
10631                    {"role": "user", "content": "continue"}
10632                ]
10633            }),
10634        )
10635        .await;
10636        assert_eq!(response.status(), AxumStatusCode::OK);
10637
10638        let request = engine.last_request();
10639        assert!(request.prompt.contains("visible answer"));
10640        assert!(!request.prompt.contains("hidden-reasoning-marker"));
10641    }
10642
10643    #[tokio::test]
10644    async fn route_tool_request_prefers_model_chat_template() {
10645        for stream in [false, true] {
10646            let template = ModelChatTemplate::new(
10647            "{% 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 %}",
10648            "tool-template",
10649        );
10650            let (router, engine) = router_with_capturing_llm_and_template(template);
10651            let response = post_json(
10652            router,
10653            "/v1/chat/completions",
10654            json!({
10655                "model": "served-alias",
10656                "messages": [
10657                    {"role": "user", "content": "Use the weather tool."},
10658                    {
10659                        "role": "assistant",
10660                        "content": null,
10661                        "tool_calls": [{
10662                            "id": "weather_paris",
10663                            "type": "function",
10664                            "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
10665                        }, {
10666                            "id": "weather_rome",
10667                            "type": "function",
10668                            "function": {"name": "weather", "arguments": "{\"city\":\"Rome\"}"}
10669                        }]
10670                    },
10671                    // Results may arrive in a different order than calls. IDs,
10672                    // rather than positions or function names, preserve pairing.
10673                    {"role": "tool", "tool_call_id": "weather_rome", "content": "rainy"},
10674                    {"role": "tool", "tool_call_id": "weather_paris", "content": "sunny"}
10675                ],
10676                "tools": [{
10677                    "type": "function",
10678                    "function": {
10679                        "name": "weather",
10680                        "description": "Get weather",
10681                        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
10682                    }
10683                }],
10684                "tool_choice": "auto",
10685                "stream": stream
10686            }),
10687        )
10688        .await;
10689            assert_eq!(response.status(), AxumStatusCode::OK);
10690
10691            if stream {
10692                let body = response_text(response).await;
10693                assert!(body.contains("[DONE]"), "{body}");
10694                assert!(body.contains("captured"), "{body}");
10695            } else {
10696                let body = response_json(response).await;
10697                assert_eq!(body["choices"][0]["message"]["content"], "captured");
10698                assert_eq!(body["choices"][0]["finish_reason"], "stop");
10699            }
10700            let request = engine.last_request();
10701            assert!(request.prompt.contains("<tools>weather</tools>"));
10702            assert!(
10703                request
10704                    .prompt
10705                    .contains("<tool_call id=\"weather_paris\">weather:"),
10706                "{}",
10707                request.prompt
10708            );
10709            assert!(request.prompt.contains("\"city\""), "{}", request.prompt);
10710            assert!(request.prompt.contains("Paris"), "{}", request.prompt);
10711            assert!(request
10712                .prompt
10713                .contains("<tool_response id=\"weather_paris\">sunny</tool_response>"));
10714            assert!(request
10715                .prompt
10716                .contains("<tool_call id=\"weather_rome\">weather:"));
10717            assert!(request.prompt.contains("Rome"));
10718            assert!(request
10719                .prompt
10720                .contains("<tool_response id=\"weather_rome\">rainy</tool_response>"));
10721            assert!(request.prompt.ends_with("[assistant]"));
10722            let Some(ferrum_types::ApiRequest::Chat(api)) = request.api_request.as_ref() else {
10723                panic!("expected structured continuation request");
10724            };
10725            assert_eq!(api.messages.len(), 4);
10726            assert_eq!(api.messages[1].tool_calls.len(), 2);
10727            for (call, id, city) in [
10728                (&api.messages[1].tool_calls[0], "weather_paris", "Paris"),
10729                (&api.messages[1].tool_calls[1], "weather_rome", "Rome"),
10730            ] {
10731                assert_eq!(call.id, id);
10732                assert_eq!(call.function.name, "weather");
10733                let args: Value = serde_json::from_str(&call.function.arguments).unwrap();
10734                assert_eq!(args, json!({"city": city}));
10735                let prefix = format!("<tool_call id=\"{id}\">weather:");
10736                let rendered_arguments = request
10737                    .prompt
10738                    .split_once(&prefix)
10739                    .unwrap()
10740                    .1
10741                    .split_once("</tool_call>")
10742                    .unwrap()
10743                    .0;
10744                let rendered: Value = serde_json::from_str(rendered_arguments).unwrap();
10745                assert_eq!(
10746                    rendered,
10747                    json!({"city": city}),
10748                    "tool arguments lost their call ID binding"
10749                );
10750            }
10751            for (message, id, content) in [
10752                (&api.messages[2], "weather_rome", "rainy"),
10753                (&api.messages[3], "weather_paris", "sunny"),
10754            ] {
10755                assert_eq!(message.role, ferrum_types::ApiMessageRole::Tool);
10756                assert_eq!(message.tool_call_id.as_deref(), Some(id));
10757                assert_eq!(message.content, content);
10758            }
10759            assert!(
10760                !request.prompt.contains("<|assistant|>"),
10761                "model-template tool prompt should not use generic fallback: {}",
10762                request.prompt
10763            );
10764            assert!(
10765                !request.prompt.contains("When a tool is needed"),
10766                "model-template tool prompt should not inject fallback tool instructions: {}",
10767                request.prompt
10768            );
10769        }
10770    }
10771
10772    #[tokio::test]
10773    async fn chat_omitted_output_budget_uses_auto_ceiling() {
10774        let (router, engine) = router_with_capturing_llm();
10775        let response = post_json(
10776            router,
10777            "/v1/chat/completions",
10778            json!({
10779                "model": "stub-model",
10780                "messages": [{"role": "user", "content": "hello"}]
10781            }),
10782        )
10783        .await;
10784        assert_eq!(response.status(), AxumStatusCode::OK);
10785
10786        let request = engine.last_request();
10787        assert_eq!(request.sampling_params.max_tokens, 4096);
10788        assert_eq!(
10789            request.metadata.get(DEFAULT_MAX_TOKENS_METADATA_KEY),
10790            Some(&serde_json::json!(true))
10791        );
10792    }
10793
10794    #[tokio::test]
10795    async fn chat_accepts_stop_string_and_max_completion_tokens() {
10796        let (router, engine) = router_with_capturing_llm();
10797        let response = post_json(
10798            router,
10799            "/v1/chat/completions",
10800            json!({
10801                "model": "stub-model",
10802                "messages": [{"role": "user", "content": "hello"}],
10803                "max_tokens": 99,
10804                "max_completion_tokens": 3,
10805                "stop": "<END>"
10806            }),
10807        )
10808        .await;
10809        assert_eq!(response.status(), AxumStatusCode::OK);
10810
10811        let request = engine.last_request();
10812        let defaults = default_chat_sampling_params();
10813        assert_eq!(request.sampling_params.max_tokens, 3);
10814        assert!(!request
10815            .metadata
10816            .contains_key(DEFAULT_MAX_TOKENS_METADATA_KEY));
10817        assert_eq!(request.sampling_params.temperature, defaults.temperature);
10818        assert_eq!(
10819            request.sampling_params.repetition_penalty,
10820            defaults.repetition_penalty
10821        );
10822        assert_eq!(request.sampling_params.stop_sequences, vec!["<END>"]);
10823    }
10824
10825    #[tokio::test]
10826    async fn chat_maps_vllm_sampling_extensions_without_hidden_defaults() {
10827        let (router, engine) = router_with_capturing_llm();
10828        let response = post_json(
10829            router,
10830            "/v1/chat/completions",
10831            json!({
10832                "model": "stub-model",
10833                "messages": [{"role": "user", "content": "hello"}],
10834                "top_k": 20,
10835                "min_p": 0.05,
10836                "repetition_penalty": 1.25
10837            }),
10838        )
10839        .await;
10840        assert_eq!(response.status(), AxumStatusCode::OK);
10841
10842        let request = engine.last_request();
10843        assert_eq!(request.sampling_params.top_k, Some(20));
10844        assert_eq!(request.sampling_params.min_p, Some(0.05));
10845        assert_eq!(request.sampling_params.repetition_penalty, 1.25);
10846    }
10847
10848    #[tokio::test]
10849    async fn chat_normalizes_disabled_sampling_extensions_and_rejects_invalid_ranges() {
10850        let (router, engine) = router_with_capturing_llm();
10851        let response = post_json(
10852            router,
10853            "/v1/chat/completions",
10854            json!({
10855                "model": "stub-model",
10856                "messages": [{"role": "user", "content": "hello"}],
10857                "top_k": -1,
10858                "min_p": 0.0,
10859                "repetition_penalty": 1.0
10860            }),
10861        )
10862        .await;
10863        assert_eq!(response.status(), AxumStatusCode::OK);
10864        let request = engine.last_request();
10865        assert_eq!(request.sampling_params.top_k, None);
10866        assert_eq!(request.sampling_params.min_p, None);
10867        assert_eq!(request.sampling_params.repetition_penalty, 1.0);
10868
10869        for (field, value) in [
10870            ("top_k", json!(-2)),
10871            ("min_p", json!(1.01)),
10872            ("repetition_penalty", json!(0.0)),
10873            ("presence_penalty", json!(2.01)),
10874            ("frequency_penalty", json!(-2.01)),
10875        ] {
10876            let (router, _) = router_with_capturing_llm();
10877            let response = post_json(
10878                router,
10879                "/v1/chat/completions",
10880                json!({
10881                    "model": "stub-model",
10882                    "messages": [{"role": "user", "content": "hello"}],
10883                    (field): value
10884                }),
10885            )
10886            .await;
10887            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST, "{field}");
10888            let body = response_json(response).await;
10889            assert_eq!(body["error"]["param"], field);
10890        }
10891    }
10892
10893    #[tokio::test]
10894    async fn chat_request_forbids_initial_think_close_token() {
10895        let engine = Arc::new(CapturingLlm::new());
10896        let router = AxumServer::from_llm(engine.clone()).build_router();
10897        let response = post_json(
10898            router,
10899            "/v1/chat/completions",
10900            json!({
10901                "model": "qwen3",
10902                "messages": [{"role": "user", "content": "hello"}]
10903            }),
10904        )
10905        .await;
10906        assert_eq!(response.status(), AxumStatusCode::OK);
10907
10908        let request = engine.last_request();
10909        assert_eq!(
10910            request
10911                .metadata
10912                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
10913            Some(&serde_json::json!([THINK_END_TAG]))
10914        );
10915    }
10916
10917    #[tokio::test]
10918    async fn omitted_enable_thinking_preserves_model_template_default() {
10919        let template = ModelChatTemplate::new(
10920            "{% 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 %}",
10921            "test-template",
10922        );
10923        let (router, engine) = router_with_capturing_llm_and_template(template);
10924        let response = post_json(
10925            router,
10926            "/v1/chat/completions",
10927            json!({
10928                "model": "served-alias",
10929                "messages": [{"role": "user", "content": "hello"}]
10930            }),
10931        )
10932        .await;
10933        assert_eq!(response.status(), AxumStatusCode::OK);
10934
10935        let request = engine.last_request();
10936        assert!(request.prompt.ends_with("<|im_start|>assistant\n<think>\n"));
10937        assert!(!request
10938            .metadata
10939            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
10940    }
10941
10942    #[tokio::test]
10943    async fn server_thinking_default_applies_but_request_override_wins() {
10944        let template = ModelChatTemplate::new(
10945            "{% 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 %}",
10946            "test-template",
10947        );
10948        let (router, engine) =
10949            router_with_capturing_llm_and_template_default(template, Some(false));
10950
10951        let response = post_json(
10952            router.clone(),
10953            "/v1/chat/completions",
10954            json!({
10955                "model": "served-alias",
10956                "messages": [{"role": "user", "content": "hello"}]
10957            }),
10958        )
10959        .await;
10960        assert_eq!(response.status(), AxumStatusCode::OK);
10961        let request = engine.last_request();
10962        assert_eq!(request.prompt, "<assistant><think>\n\n</think>\n\n");
10963        assert_eq!(
10964            request
10965                .metadata
10966                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
10967            Some(&serde_json::json!([THINK_END_TAG, THINK_START_TAG]))
10968        );
10969
10970        let response = post_json(
10971            router,
10972            "/v1/chat/completions",
10973            json!({
10974                "model": "served-alias",
10975                "messages": [{"role": "user", "content": "hello"}],
10976                "chat_template_kwargs": {"enable_thinking": true}
10977            }),
10978        )
10979        .await;
10980        assert_eq!(response.status(), AxumStatusCode::OK);
10981        let request = engine.last_request();
10982        assert_eq!(request.prompt, "<assistant><think>\n");
10983        assert!(!request
10984            .metadata
10985            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
10986    }
10987
10988    #[tokio::test]
10989    async fn chat_template_enable_thinking_true_overrides_default() {
10990        let template = ModelChatTemplate::new(
10991            "{% 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 %}",
10992            "test-template",
10993        );
10994        let (router, engine) = router_with_capturing_llm_and_template(template);
10995        let response = post_json(
10996            router,
10997            "/v1/chat/completions",
10998            json!({
10999                "model": "served-alias",
11000                "messages": [{"role": "user", "content": "hello"}],
11001                "chat_template_kwargs": {"enable_thinking": true}
11002            }),
11003        )
11004        .await;
11005        assert_eq!(response.status(), AxumStatusCode::OK);
11006
11007        let request = engine.last_request();
11008        assert_eq!(request.prompt, "<assistant><think>\n");
11009        assert!(!request
11010            .metadata
11011            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
11012    }
11013
11014    #[tokio::test]
11015    async fn chat_template_enable_thinking_false_is_a_hard_override() {
11016        let template = ModelChatTemplate::new(
11017            "{% 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 %}",
11018            "test-template",
11019        );
11020        let (router, engine) = router_with_capturing_llm_and_template(template);
11021        let response = post_json(
11022            router,
11023            "/v1/chat/completions",
11024            json!({
11025                "model": "served-alias",
11026                "messages": [{"role": "user", "content": "hello"}],
11027                "chat_template_kwargs": {"enable_thinking": false}
11028            }),
11029        )
11030        .await;
11031        assert_eq!(response.status(), AxumStatusCode::OK);
11032
11033        let request = engine.last_request();
11034        assert_eq!(request.prompt, "<assistant><think>\n\n</think>\n\n");
11035        assert_eq!(
11036            request
11037                .metadata
11038                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
11039            Some(&serde_json::json!([THINK_END_TAG, THINK_START_TAG]))
11040        );
11041    }
11042
11043    #[tokio::test]
11044    async fn chat_template_reasoning_effort_is_typed_and_rendered() {
11045        let template = ModelChatTemplate::new(
11046            "{% if reasoning_effort is defined %}Reasoning: {{ reasoning_effort }}{% else %}Reasoning: model-default{% endif %}",
11047            "test-template",
11048        );
11049        let (router, engine) = router_with_capturing_llm_and_template(template);
11050        let response = post_json(
11051            router.clone(),
11052            "/v1/chat/completions",
11053            json!({
11054                "model": "served-alias",
11055                "messages": [{"role": "user", "content": "hello"}],
11056                "chat_template_kwargs": {"reasoning_effort": "low"}
11057            }),
11058        )
11059        .await;
11060        assert_eq!(response.status(), AxumStatusCode::OK);
11061        assert_eq!(engine.last_request().prompt, "Reasoning: low");
11062
11063        let response = post_json(
11064            router.clone(),
11065            "/v1/chat/completions",
11066            json!({
11067                "model": "served-alias",
11068                "messages": [{"role": "user", "content": "hello"}],
11069                "chat_template_kwargs": {"reasoning_effort": "xhigh"}
11070            }),
11071        )
11072        .await;
11073        assert_eq!(response.status(), AxumStatusCode::OK);
11074        assert_eq!(engine.last_request().prompt, "Reasoning: xhigh");
11075
11076        for invalid in [json!("extreme"), json!(1)] {
11077            let response = post_json(
11078                router.clone(),
11079                "/v1/chat/completions",
11080                json!({
11081                    "model": "served-alias",
11082                    "messages": [{"role": "user", "content": "hello"}],
11083                    "chat_template_kwargs": {"reasoning_effort": invalid}
11084                }),
11085            )
11086            .await;
11087            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11088            let body = response_json(response).await;
11089            assert_eq!(body["error"]["type"], "invalid_request_error");
11090            assert!(body["error"]["message"]
11091                .as_str()
11092                .unwrap_or_default()
11093                .contains("reasoning_effort"));
11094        }
11095    }
11096
11097    #[tokio::test]
11098    async fn chat_template_enable_thinking_rejects_non_bool() {
11099        let template = ModelChatTemplate::new(
11100            "{% if add_generation_prompt %}<assistant>{% endif %}",
11101            "test-template",
11102        );
11103        let (router, _) = router_with_capturing_llm_and_template(template);
11104        let response = post_json(
11105            router,
11106            "/v1/chat/completions",
11107            json!({
11108                "model": "served-alias",
11109                "messages": [{"role": "user", "content": "hello"}],
11110                "chat_template_kwargs": {"enable_thinking": "false"}
11111            }),
11112        )
11113        .await;
11114        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11115        let body = response_json(response).await;
11116        assert_eq!(body["error"]["type"], "invalid_request_error");
11117        assert!(body["error"]["message"]
11118            .as_str()
11119            .unwrap_or_default()
11120            .contains("chat_template_kwargs.enable_thinking must be a boolean"));
11121    }
11122
11123    #[tokio::test]
11124    async fn stop_string_strips_chat_and_completion_suffixes() {
11125        let chat = post_json(
11126            router_with_stub("hello<END>"),
11127            "/v1/chat/completions",
11128            json!({
11129                "model": "stub-model",
11130                "messages": [{"role": "user", "content": "hello"}],
11131                "stop": "<END>"
11132            }),
11133        )
11134        .await;
11135        assert_eq!(chat.status(), AxumStatusCode::OK);
11136        let chat_body = response_json(chat).await;
11137        assert_eq!(chat_body["choices"][0]["message"]["content"], "hello");
11138
11139        let completion = post_json(
11140            router_with_stub("done<END>"),
11141            "/v1/completions",
11142            json!({
11143                "model": "stub-model",
11144                "prompt": "complete",
11145                "stop": "<END>"
11146            }),
11147        )
11148        .await;
11149        assert_eq!(completion.status(), AxumStatusCode::OK);
11150        let completion_body = response_json(completion).await;
11151        assert_eq!(completion_body["choices"][0]["text"], "done");
11152    }
11153
11154    #[test]
11155    fn started_in_think_parse_streams_reasoning_before_end_tag() {
11156        // R1-distill templates open `<think>` inside the prompt, so the
11157        // generated text never contains the start tag. Mid-think text must
11158        // be reasoning, not content (this leaked as content deltas before).
11159        let parsed = parse_reasoning_response_started_in_think("Okay, the user wants");
11160        assert_eq!(parsed.reasoning.as_deref(), Some("Okay, the user wants"));
11161        assert_eq!(parsed.content, "");
11162
11163        let parsed = parse_reasoning_response_started_in_think("thinking...</think>\nanswer");
11164        assert_eq!(parsed.reasoning.as_deref(), Some("thinking..."));
11165        assert_eq!(parsed.content, "answer");
11166
11167        // Model re-opening its own think block defers to the normal parse.
11168        let parsed = parse_reasoning_response_started_in_think("<think>\nx\n</think>\n\nanswer");
11169        assert_eq!(parsed.reasoning.as_deref(), Some("\nx\n"));
11170        assert_eq!(parsed.content, "answer");
11171    }
11172
11173    #[tokio::test]
11174    async fn chat_response_splits_reasoning_from_content() {
11175        let response = post_json(
11176            router_with_stub("<think>\nreasoning\n</think>\n\nfinal answer"),
11177            "/v1/chat/completions",
11178            json!({
11179                "model": "stub-model",
11180                "messages": [{"role": "user", "content": "hello"}]
11181            }),
11182        )
11183        .await;
11184        assert_eq!(response.status(), AxumStatusCode::OK);
11185
11186        let body = response_json(response).await;
11187        let message = &body["choices"][0]["message"];
11188        assert_eq!(message["content"], "final answer");
11189        assert_eq!(message["reasoning"], "\nreasoning\n");
11190        assert!(message.get("reasoning_content").is_none());
11191    }
11192
11193    #[tokio::test]
11194    async fn streaming_chat_reasoning_prefix_chunks_do_not_panic_or_leak_content() {
11195        let response = post_json(
11196            router_with_stub_stream_chunks(&["<", "think", ">\nreason", "\n</think>\n\nfinal"]),
11197            "/v1/chat/completions",
11198            json!({
11199                "model": "stub-model",
11200                "messages": [{"role": "user", "content": "think then answer"}],
11201                "stream": true
11202            }),
11203        )
11204        .await;
11205        assert_eq!(response.status(), AxumStatusCode::OK);
11206        let body = response_text(response).await;
11207        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
11208        assert!(
11209            body.contains(r#""reasoning":"\nreason"#),
11210            "stream should emit reasoning delta after full think prefix: {body}"
11211        );
11212        assert!(!body.contains("\"reasoning_content\":"));
11213        assert!(
11214            body.contains(r#""content":"final""#),
11215            "stream should emit visible content after think close: {body}"
11216        );
11217        assert!(
11218            !body.contains(r#""content":"<"#),
11219            "partial think prefix must not leak as content: {body}"
11220        );
11221    }
11222
11223    #[tokio::test]
11224    async fn route_rejects_unsupported_tool_and_function_selection() {
11225        for (extra, param) in [
11226            (
11227                json!({
11228                    "tools": [{
11229                        "type": "function",
11230                        "function": {"name": "weather", "parameters": {"type": "object"}}
11231                    }],
11232                    "tool_choice": {
11233                        "type": "function",
11234                        "function": {"name": "calendar"}
11235                    }
11236                }),
11237                "tool_choice",
11238            ),
11239            (
11240                json!({
11241                    "functions": [{"name": "weather", "parameters": {"type": "object"}}],
11242                    "function_call": {"name": "calendar"}
11243                }),
11244                "function_call",
11245            ),
11246        ] {
11247            let mut body = json!({
11248                "model": "stub-model",
11249                "messages": [{"role": "user", "content": "hello"}]
11250            });
11251            body.as_object_mut()
11252                .expect("object")
11253                .extend(extra.as_object().expect("extra object").clone());
11254            let response =
11255                post_json(router_with_stub("unused"), "/v1/chat/completions", body).await;
11256            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11257            let body = response_json(response).await;
11258            assert_eq!(body["error"]["type"], "invalid_request_error");
11259            assert_eq!(body["error"]["param"], param);
11260        }
11261    }
11262
11263    #[tokio::test]
11264    async fn route_rejects_non_function_tools_with_openai_error_param() {
11265        let response = post_json(
11266            router_with_stub("unused"),
11267            "/v1/chat/completions",
11268            json!({
11269                "model": "stub-model",
11270                "messages": [{"role": "user", "content": "hello"}],
11271                "tools": [{
11272                    "type": "retrieval",
11273                    "function": {"name": "search", "parameters": {"type": "object"}}
11274                }]
11275            }),
11276        )
11277        .await;
11278        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11279        let body = response_json(response).await;
11280        assert_eq!(body["error"]["type"], "invalid_request_error");
11281        assert_eq!(body["error"]["param"], "tools");
11282    }
11283
11284    #[tokio::test]
11285    async fn route_rejects_tool_choice_required_without_tools() {
11286        let response = post_json(
11287            router_with_stub("unused"),
11288            "/v1/chat/completions",
11289            json!({
11290                "model": "stub-model",
11291                "messages": [{"role": "user", "content": "hello"}],
11292                "tool_choice": "required"
11293            }),
11294        )
11295        .await;
11296        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11297        let body = response_json(response).await;
11298        assert_eq!(body["error"]["type"], "invalid_request_error");
11299        assert_eq!(body["error"]["param"], "tool_choice");
11300    }
11301
11302    #[tokio::test]
11303    async fn route_rejects_unknown_response_format_type_with_openai_error_param() {
11304        let response = post_json(
11305            router_with_stub("unused"),
11306            "/v1/chat/completions",
11307            json!({
11308                "model": "stub-model",
11309                "messages": [{"role": "user", "content": "hello"}],
11310                "response_format": {"type": "xml"}
11311            }),
11312        )
11313        .await;
11314        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11315        let body = response_json(response).await;
11316        assert_eq!(body["error"]["type"], "invalid_request_error");
11317        assert_eq!(body["error"]["param"], "response_format.type");
11318    }
11319
11320    #[tokio::test]
11321    async fn route_chat_engine_unavailable_maps_to_503() {
11322        let response = post_json(
11323            router_without_llm(),
11324            "/v1/chat/completions",
11325            json!({
11326                "model": "stub-model",
11327                "messages": [{"role": "user", "content": "hello"}]
11328            }),
11329        )
11330        .await;
11331        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11332        let body = response_json(response).await;
11333        assert_eq!(body["error"]["type"], "service_unavailable_error");
11334        assert_eq!(body["error"]["param"], Value::Null);
11335    }
11336
11337    #[tokio::test]
11338    async fn context_capacity_rejection_has_a_structured_code_on_openai_routes() {
11339        for stream in [false, true] {
11340            for (path, mut input) in [
11341                (
11342                    "/v1/chat/completions",
11343                    json!({"messages":[{"role":"user","content":"hello"}],"max_tokens":100}),
11344                ),
11345                (
11346                    "/v1/completions",
11347                    json!({"prompt":"hello","max_tokens":100}),
11348                ),
11349                (
11350                    "/v1/responses",
11351                    json!({"input":"hello","max_output_tokens":100}),
11352                ),
11353            ] {
11354                input["model"] = json!("failing-model");
11355                input["stream"] = json!(stream);
11356                let router = AxumServer::from_llm(Arc::new(FailingLlm::context_length_exceeded()))
11357                    .build_router();
11358                let response = post_json(router, path, input).await;
11359                assert_eq!(
11360                    response.status(),
11361                    AxumStatusCode::BAD_REQUEST,
11362                    "{path}, stream={stream}"
11363                );
11364                let body = response_json(response).await;
11365                assert_eq!(body["error"]["code"], "context_length_exceeded", "{body}");
11366                assert_eq!(body["error"]["type"], "invalid_request_error");
11367                assert!(body["error"]["message"]
11368                    .as_str()
11369                    .unwrap()
11370                    .contains("500 input tokens + 100 output tokens"));
11371            }
11372        }
11373        let ordinary =
11374            server_error_from_ferrum_error(Error::request_validation("invalid parameter"))
11375                .into_response();
11376        assert_eq!(response_json(ordinary).await["error"]["code"], Value::Null);
11377    }
11378
11379    #[tokio::test]
11380    async fn route_chat_generation_failure_maps_to_500() {
11381        let response = post_json(
11382            router_with_failing_llm(),
11383            "/v1/chat/completions",
11384            json!({
11385                "model": "failing-model",
11386                "messages": [{"role": "user", "content": "hello"}]
11387            }),
11388        )
11389        .await;
11390        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11391        let body = response_json(response).await;
11392        assert_eq!(body["error"]["type"], "internal_server_error");
11393        assert!(body["error"]["message"]
11394            .as_str()
11395            .unwrap()
11396            .contains("stub generation failed"));
11397    }
11398
11399    #[tokio::test]
11400    async fn route_chat_generation_failure_writes_replay_diagnostics() {
11401        let root = unique_request_dump_dir("chat-sync-failure");
11402        let profile = unique_profile_jsonl("chat-sync-failure");
11403        let response = post_json(
11404            router_with_failing_llm_request_dump_and_profile(root.clone(), profile.clone()),
11405            "/v1/chat/completions",
11406            json!({
11407                "model": "failing-model",
11408                "messages": [{"role": "user", "content": "hello"}]
11409            }),
11410        )
11411        .await;
11412        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11413        assert_chat_failure_replay_bundle(
11414            &root,
11415            "chat_completions_sync",
11416            "internal",
11417            "stub generation failed",
11418        );
11419        let event = read_profile_events(&profile)
11420            .into_iter()
11421            .find(|event| event["phase"] == "chat_completions_sync")
11422            .expect("sync failure profile event");
11423        assert_eq!(event["event_kind"], "timed_span");
11424        assert_eq!(event["status"], "failure");
11425        assert!(event["duration_us"].as_u64().is_some());
11426        assert_eq!(event["attributes"]["terminal_failure_event"], true);
11427        assert_eq!(event["error"]["kind"], "internal");
11428        let _ = fs::remove_dir_all(root);
11429        let _ = fs::remove_file(profile);
11430    }
11431
11432    #[tokio::test]
11433    async fn route_chat_resource_failure_writes_resource_replay_diagnostics() {
11434        let root = unique_request_dump_dir("chat-resource-failure");
11435        let response = post_json(
11436            router_with_resource_exhausted_llm_and_request_dump_dir(root.clone()),
11437            "/v1/chat/completions",
11438            json!({
11439                "model": "failing-model",
11440                "messages": [{"role": "user", "content": "hello"}]
11441            }),
11442        )
11443        .await;
11444        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11445        let bundle = only_replay_bundle(&root);
11446        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
11447        assert_eq!(bad_scan["failure_kind"], "oom_admission");
11448        let diagnostics = read_json_file(bundle.join("failure_diagnostics.json"));
11449        assert_eq!(diagnostics["failure_kind"], "oom_admission");
11450        assert_eq!(
11451            diagnostics["first_failure_event"]["error_kind"],
11452            "resource_exhausted"
11453        );
11454        assert_eq!(
11455            diagnostics["capacity"]["resource_kind"],
11456            "admission_capacity"
11457        );
11458        assert!(diagnostics["capacity"]["reason"]
11459            .as_str()
11460            .expect("capacity reason")
11461            .contains("admission capacity exhausted"));
11462        assert_eq!(
11463            diagnostics["nearest_resource_event"]["resource_kind"],
11464            "admission_capacity"
11465        );
11466        assert!(diagnostics["nearest_memory_snapshot"]["current_bytes"].is_number());
11467        assert!(diagnostics["nearest_memory_snapshot"]["high_water_bytes"].is_number());
11468        let _ = fs::remove_dir_all(root);
11469    }
11470
11471    #[tokio::test]
11472    async fn route_chat_sync_success_updates_replay_output_tokens() {
11473        let root = unique_request_dump_dir("chat-sync-success-output");
11474        let response = post_json(
11475            router_with_stub_and_request_dump_dir("OK", root.clone()),
11476            "/v1/chat/completions",
11477            json!({
11478                "model": "stub-model",
11479                "messages": [{"role": "user", "content": "hello"}]
11480            }),
11481        )
11482        .await;
11483        assert_eq!(response.status(), AxumStatusCode::OK);
11484        let body = response_json(response).await;
11485        assert_eq!(body["choices"][0]["message"]["content"], "OK");
11486        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "OK");
11487        let _ = fs::remove_dir_all(root);
11488    }
11489
11490    #[tokio::test]
11491    async fn route_chat_sync_success_writes_product_profile_event() {
11492        let root = unique_request_dump_dir("chat-sync-profile");
11493        let profile = unique_profile_jsonl("chat-sync-profile");
11494        let response = post_json(
11495            router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone()),
11496            "/v1/chat/completions",
11497            json!({
11498                "model": "stub-model",
11499                "messages": [{"role": "user", "content": "hello"}]
11500            }),
11501        )
11502        .await;
11503        assert_eq!(response.status(), AxumStatusCode::OK);
11504        let response_body = response_json(response).await;
11505
11506        let events = read_profile_events(&profile);
11507        assert_eq!(events.len(), 2, "events: {events:#?}");
11508        let event = events
11509            .iter()
11510            .find(|event| event["phase"] == "chat_completions_sync_complete")
11511            .expect("sync completion profile event");
11512        assert!(response_body["id"]
11513            .as_str()
11514            .is_some_and(|id| !id.is_empty()));
11515        assert_eq!(response_body["id"], event["request_id"]);
11516        assert_eq!(response_body["id"], event["correlation_id"]);
11517        assert_eq!(
11518            event["schema_version"],
11519            OBSERVABILITY_PROFILE_SCHEMA_VERSION
11520        );
11521        assert_eq!(event["entrypoint"], "serve");
11522        assert_eq!(event["event_kind"], "timed_span");
11523        assert_eq!(event["status"], "ok");
11524        assert_eq!(event["phase"], "chat_completions_sync_complete");
11525        assert_eq!(event["attributes"]["actual_model_smoke"], true);
11526        assert_eq!(event["attributes"]["profile_detail"], "latency");
11527        assert_eq!(event["attributes"]["diagnostic_only"], false);
11528        assert_eq!(event["attributes"]["stream"], false);
11529        assert_eq!(event["attributes"]["output_token_count"], 2);
11530        assert_eq!(event["attributes"]["prompt_token_count"], 7);
11531        assert_eq!(event["attributes"]["completion_token_count"], 2);
11532        assert_eq!(event["attributes"]["total_token_count"], 9);
11533        assert_eq!(event["attributes"]["token_count_source"], "usage");
11534        assert_eq!(event["attributes"]["finish_reason"], "stop");
11535        assert_eq!(
11536            event["attributes"]["engine_token_clock_source"],
11537            "rust_std_instant"
11538        );
11539        assert_eq!(event["attributes"]["engine_token_commit_count"], 2);
11540        assert_eq!(event["attributes"]["itl_interval_count"], 1);
11541        assert_eq!(event["attributes"]["ttft_us"], 1_000);
11542        assert_eq!(event["attributes"]["itl_us_avg"], 1_000);
11543        assert!(event["attributes"]["http_first_sse_enqueue_us"].is_null());
11544        assert!(event["duration_us"].as_u64().unwrap_or_default() > 0);
11545        assert!(
11546            event["attributes"]["e2e_duration_us"]
11547                .as_u64()
11548                .unwrap_or_default()
11549                > 0
11550        );
11551        assert_eq!(
11552            event["replay"]["bundle_dir"].as_str(),
11553            Some(root.to_string_lossy().as_ref())
11554        );
11555        assert!(event["replay"]["command"]
11556            .as_str()
11557            .unwrap_or_default()
11558            .contains("replay_body.json"));
11559        let memory_event = events
11560            .iter()
11561            .find(|event| event["phase"] == "actual_serve_first_request_done")
11562            .expect("first request memory profile event");
11563        assert_eq!(memory_event["event_kind"], "memory");
11564        assert_eq!(
11565            memory_event["attributes"]["memory_stage"],
11566            "first_request_done"
11567        );
11568        assert_eq!(
11569            memory_event["attributes"]["memory_measurement"],
11570            "process_rss"
11571        );
11572        assert!(memory_event["memory"]["current_bytes"]
11573            .as_u64()
11574            .is_some_and(|bytes| bytes > 0));
11575        let _ = fs::remove_dir_all(root);
11576        let _ = fs::remove_file(profile);
11577    }
11578
11579    #[tokio::test]
11580    async fn route_chat_profile_events_preserve_benchmark_correlation() {
11581        let root = unique_request_dump_dir("chat-benchmark-correlation");
11582        let profile = unique_profile_jsonl("chat-benchmark-correlation");
11583        let correlation = BenchmarkRequestCorrelation::new(
11584            "bench-123".to_string(),
11585            "cell-1-closed-c8".to_string(),
11586            2,
11587            ferrum_bench_core::BenchmarkPhase::Measured,
11588            17,
11589        )
11590        .unwrap();
11591        let response = post_json_with_benchmark_correlation(
11592            router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone()),
11593            "/v1/chat/completions",
11594            json!({
11595                "model": "stub-model",
11596                "messages": [{"role": "user", "content": "hello"}]
11597            }),
11598            &correlation,
11599        )
11600        .await;
11601        assert_eq!(response.status(), AxumStatusCode::OK);
11602        let _ = response_json(response).await;
11603
11604        let events = read_profile_events(&profile);
11605        assert_eq!(events.len(), 2, "events: {events:#?}");
11606        for event in events {
11607            assert_eq!(event["attributes"]["benchmark_run_id"], "bench-123");
11608            assert_eq!(event["attributes"]["cell_id"], "cell-1-closed-c8");
11609            assert_eq!(event["attributes"]["repeat_index"], 2);
11610            assert_eq!(event["attributes"]["phase"], "measured");
11611            assert_eq!(event["attributes"]["request_index"], 17);
11612        }
11613        let _ = fs::remove_dir_all(root);
11614        let _ = fs::remove_file(profile);
11615    }
11616
11617    #[tokio::test]
11618    async fn route_chat_rejects_partial_benchmark_correlation_headers() {
11619        let response = router_with_stub("OK")
11620            .oneshot(
11621                Request::builder()
11622                    .method("POST")
11623                    .uri("/v1/chat/completions")
11624                    .header(header::CONTENT_TYPE, "application/json")
11625                    .header(BENCHMARK_RUN_ID_HEADER, "bench-123")
11626                    .body(Body::from(
11627                        json!({
11628                            "model": "stub-model",
11629                            "messages": [{"role": "user", "content": "hello"}]
11630                        })
11631                        .to_string(),
11632                    ))
11633                    .expect("request"),
11634            )
11635            .await
11636            .expect("route response");
11637        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11638    }
11639
11640    #[tokio::test]
11641    async fn route_chat_sync_profile_jsonl_is_parseable_under_concurrent_requests() {
11642        let root = unique_request_dump_dir("chat-sync-profile-concurrent");
11643        let profile = unique_profile_jsonl("chat-sync-profile-concurrent");
11644        let app = router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone());
11645
11646        let mut handles = Vec::new();
11647        for request_index in 0..8 {
11648            let app = app.clone();
11649            handles.push(tokio::spawn(async move {
11650                let response = post_json(
11651                    app,
11652                    "/v1/chat/completions",
11653                    json!({
11654                        "model": "stub-model",
11655                        "messages": [{"role": "user", "content": format!("hello {request_index}")}]
11656                    }),
11657                )
11658                .await;
11659                assert_eq!(response.status(), AxumStatusCode::OK);
11660                let body = response_json(response).await;
11661                assert_eq!(body["choices"][0]["message"]["content"], "OK");
11662            }));
11663        }
11664
11665        for handle in handles {
11666            handle.await.expect("concurrent request task");
11667        }
11668
11669        let raw = fs::read_to_string(&profile).expect("profile jsonl");
11670        let mut completion_events = 0usize;
11671        for (line_index, line) in raw
11672            .lines()
11673            .filter(|line| !line.trim().is_empty())
11674            .enumerate()
11675        {
11676            let event: Value = serde_json::from_str(line).unwrap_or_else(|err| {
11677                panic!(
11678                    "profile line {} invalid JSON: {err}: {line}",
11679                    line_index + 1
11680                )
11681            });
11682            if event["phase"] == "chat_completions_sync_complete" {
11683                completion_events += 1;
11684            }
11685        }
11686        assert_eq!(completion_events, 8);
11687        let _ = fs::remove_dir_all(root);
11688        let _ = fs::remove_file(profile);
11689    }
11690
11691    #[tokio::test]
11692    async fn route_chat_stream_success_updates_replay_output_tokens() {
11693        let root = unique_request_dump_dir("chat-stream-success-output");
11694        let response = post_json(
11695            router_with_stub_stream_chunks_and_request_dump_dir(&["O", "K"], root.clone()),
11696            "/v1/chat/completions",
11697            json!({
11698                "model": "stub-model",
11699                "messages": [{"role": "user", "content": "hello"}],
11700                "stream": true
11701            }),
11702        )
11703        .await;
11704        assert_eq!(response.status(), AxumStatusCode::OK);
11705        let body = response_text(response).await;
11706        assert!(body.contains("data: [DONE]"), "body: {body}");
11707        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "OK");
11708        let _ = fs::remove_dir_all(root);
11709    }
11710
11711    #[tokio::test]
11712    async fn route_chat_stream_success_writes_product_profile_event() {
11713        let root = unique_request_dump_dir("chat-stream-profile");
11714        let profile = unique_profile_jsonl("chat-stream-profile");
11715        let response = post_json(
11716            router_with_stub_stream_request_dump_and_profile(
11717                &["O", "K"],
11718                root.clone(),
11719                profile.clone(),
11720            ),
11721            "/v1/chat/completions",
11722            json!({
11723                "model": "stub-model",
11724                "messages": [{"role": "user", "content": "hello"}],
11725                "stream": true
11726            }),
11727        )
11728        .await;
11729        assert_eq!(response.status(), AxumStatusCode::OK);
11730        let body = response_text(response).await;
11731        assert!(body.contains("data: [DONE]"), "body: {body}");
11732
11733        let events = read_profile_events(&profile);
11734        assert_eq!(events.len(), 2, "events: {events:#?}");
11735        let event = events
11736            .iter()
11737            .find(|event| event["phase"] == "chat_completions_stream_complete")
11738            .expect("stream completion profile event");
11739        let chunks = responses_sse_json_events(&body);
11740        assert!(!chunks.is_empty());
11741        for chunk in chunks {
11742            assert!(chunk["id"].as_str().is_some_and(|id| !id.is_empty()));
11743            assert_eq!(chunk["id"], event["request_id"]);
11744            assert_eq!(chunk["id"], event["correlation_id"]);
11745        }
11746        assert_eq!(
11747            event["schema_version"],
11748            OBSERVABILITY_PROFILE_SCHEMA_VERSION
11749        );
11750        assert_eq!(event["entrypoint"], "serve");
11751        assert_eq!(event["event_kind"], "timed_span");
11752        assert_eq!(event["status"], "ok");
11753        assert_eq!(event["phase"], "chat_completions_stream_complete");
11754        assert_eq!(event["attributes"]["actual_model_smoke"], true);
11755        assert_eq!(event["attributes"]["profile_detail"], "latency");
11756        assert_eq!(event["attributes"]["diagnostic_only"], false);
11757        assert_eq!(event["attributes"]["stream"], true);
11758        assert_eq!(event["attributes"]["output_token_count"], 2);
11759        assert_eq!(event["attributes"]["prompt_token_count"], 5);
11760        assert_eq!(event["attributes"]["completion_token_count"], 2);
11761        assert_eq!(event["attributes"]["total_token_count"], 7);
11762        assert_eq!(event["attributes"]["token_count_source"], "usage");
11763        assert_eq!(event["attributes"]["finish_reason"], "stop");
11764        assert!(event["duration_us"].as_u64().unwrap_or_default() > 0);
11765        assert!(
11766            event["attributes"]["e2e_duration_us"]
11767                .as_u64()
11768                .unwrap_or_default()
11769                > 0
11770        );
11771        assert!(event["attributes"]["ttft_us"].as_u64().is_some());
11772        assert!(event["attributes"]["itl_us_avg"].as_u64().is_some());
11773        assert_eq!(
11774            event["attributes"]["engine_token_commit_nanos_since_request_start"],
11775            json!([1_000_000, 2_000_000])
11776        );
11777        assert_eq!(event["attributes"]["itl_interval_count"], 1);
11778        assert_eq!(event["attributes"]["itl_source"], "engine_token_commit");
11779        assert!(event["attributes"]["engine_stream_first_chunk_received_us"]
11780            .as_u64()
11781            .is_some());
11782        assert!(event["attributes"]["http_first_sse_enqueue_us"]
11783            .as_u64()
11784            .is_some());
11785        assert!(event["attributes"]["http_stream_flush_unavailable_reason"]
11786            .as_str()
11787            .is_some());
11788        assert_eq!(
11789            event["replay"]["bundle_dir"].as_str(),
11790            Some(root.to_string_lossy().as_ref())
11791        );
11792        let memory_event = events
11793            .iter()
11794            .find(|event| event["phase"] == "actual_serve_first_request_done")
11795            .expect("first request memory profile event");
11796        assert_eq!(memory_event["event_kind"], "memory");
11797        assert_eq!(
11798            memory_event["attributes"]["memory_stage"],
11799            "first_request_done"
11800        );
11801        assert_eq!(
11802            memory_event["attributes"]["memory_measurement"],
11803            "process_rss"
11804        );
11805        assert!(memory_event["memory"]["current_bytes"]
11806            .as_u64()
11807            .is_some_and(|bytes| bytes > 0));
11808        let _ = fs::remove_dir_all(root);
11809        let _ = fs::remove_file(profile);
11810    }
11811
11812    #[tokio::test]
11813    async fn route_chat_stream_profile_retains_non_visible_terminal_token() {
11814        let root = unique_request_dump_dir("chat-stream-profile-terminal-token");
11815        let profile = unique_profile_jsonl("chat-stream-profile-terminal-token");
11816        let llm = StubLlm {
11817            stream_usage: Some(TokenUsage::new(5, 2)),
11818            ..StubLlm::with_stream_chunks(&["Paris"])
11819        };
11820        let app = AxumServer::from_state(
11821            AppState::default()
11822                .with_llm(Arc::new(llm))
11823                .with_request_dump_dir(Some(root.clone()))
11824                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
11825                .with_profile_jsonl(Some(profile.clone())),
11826        )
11827        .build_router();
11828
11829        let response = post_json(
11830            app,
11831            "/v1/chat/completions",
11832            json!({
11833                "model": "stub-model",
11834                "messages": [{"role": "user", "content": "hello"}],
11835                "stream": true,
11836                "stream_options": {"include_usage": true}
11837            }),
11838        )
11839        .await;
11840        assert_eq!(response.status(), AxumStatusCode::OK);
11841        let body = response_text(response).await;
11842        assert!(body.contains("\"completion_tokens\":2"), "body: {body}");
11843        assert!(body.contains("data: [DONE]"), "body: {body}");
11844
11845        let events = read_profile_events(&profile);
11846        let event = events
11847            .iter()
11848            .find(|event| event["phase"] == "chat_completions_stream_complete")
11849            .expect("stream completion profile event");
11850        assert_eq!(event["attributes"]["output_token_count"], 2);
11851        assert_eq!(event["attributes"]["completion_token_count"], 2);
11852        assert_eq!(event["attributes"]["engine_token_commit_count"], 2);
11853        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "Paris");
11854
11855        let _ = fs::remove_dir_all(root);
11856        let _ = fs::remove_file(profile);
11857    }
11858
11859    #[tokio::test]
11860    async fn route_chat_sync_bad_output_updates_replay_classifier() {
11861        let root = unique_request_dump_dir("chat-sync-bad-output");
11862        let response = post_json(
11863            router_with_stub_and_request_dump_dir("<unk>", root.clone()),
11864            "/v1/chat/completions",
11865            json!({
11866                "model": "stub-model",
11867                "messages": [{"role": "user", "content": "hello"}]
11868            }),
11869        )
11870        .await;
11871        assert_eq!(response.status(), AxumStatusCode::OK);
11872        let _ = response_json(response).await;
11873        let bundle = only_replay_bundle(&root);
11874        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
11875        assert_eq!(bad_scan["bad_output"], true);
11876        assert_eq!(bad_scan["reasons"], json!(["reserved_token"]));
11877        assert_eq!(bad_scan["first_bad_text_span"]["reason"], "reserved_token");
11878        let _ = fs::remove_dir_all(root);
11879    }
11880
11881    #[tokio::test]
11882    async fn route_chat_stream_generation_failure_emits_openai_error_event() {
11883        let response = post_json(
11884            router_with_failing_llm(),
11885            "/v1/chat/completions",
11886            json!({
11887                "model": "failing-model",
11888                "messages": [{"role": "user", "content": "hello"}],
11889                "stream": true
11890            }),
11891        )
11892        .await;
11893        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11894        let body = response_json(response).await;
11895        assert_eq!(body["error"]["type"], "internal_server_error");
11896        assert!(body["error"]["message"]
11897            .as_str()
11898            .unwrap_or_default()
11899            .contains("stub stream failed"));
11900    }
11901
11902    #[tokio::test]
11903    async fn route_chat_stream_generation_failure_writes_replay_diagnostics() {
11904        let root = unique_request_dump_dir("chat-stream-start-failure");
11905        let response = post_json(
11906            router_with_failing_llm_and_request_dump_dir(root.clone()),
11907            "/v1/chat/completions",
11908            json!({
11909                "model": "failing-model",
11910                "messages": [{"role": "user", "content": "hello"}],
11911                "stream": true
11912            }),
11913        )
11914        .await;
11915        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11916        let body = response_json(response).await;
11917        assert_eq!(body["error"]["type"], "internal_server_error");
11918        assert!(body["error"]["message"]
11919            .as_str()
11920            .unwrap_or_default()
11921            .contains("stub stream failed"));
11922        assert_chat_failure_replay_bundle(
11923            &root,
11924            "chat_completions_stream_start",
11925            "internal",
11926            "stub stream failed",
11927        );
11928        let _ = fs::remove_dir_all(root);
11929    }
11930
11931    #[tokio::test]
11932    async fn route_chat_stream_chunk_failure_emits_openai_error_event() {
11933        let response = post_json(
11934            router_with_stream_chunk_failing_llm(),
11935            "/v1/chat/completions",
11936            json!({
11937                "model": "failing-model",
11938                "messages": [{"role": "user", "content": "hello"}],
11939                "stream": true
11940            }),
11941        )
11942        .await;
11943        assert_eq!(response.status(), AxumStatusCode::OK);
11944        let body = response_text(response).await;
11945        assert_openai_stream_error(&body, "stub stream chunk failed");
11946    }
11947
11948    #[tokio::test]
11949    async fn route_chat_stream_chunk_failure_writes_replay_diagnostics() {
11950        let root = unique_request_dump_dir("chat-stream-chunk-failure");
11951        let response = post_json(
11952            router_with_stream_chunk_failing_llm_and_request_dump_dir(root.clone()),
11953            "/v1/chat/completions",
11954            json!({
11955                "model": "failing-model",
11956                "messages": [{"role": "user", "content": "hello"}],
11957                "stream": true
11958            }),
11959        )
11960        .await;
11961        assert_eq!(response.status(), AxumStatusCode::OK);
11962        let body = response_text(response).await;
11963        assert_openai_stream_error(&body, "stub stream chunk failed");
11964        assert_chat_failure_replay_bundle(
11965            &root,
11966            "chat_completions_stream_next",
11967            "internal",
11968            "stub stream chunk failed",
11969        );
11970        let _ = fs::remove_dir_all(root);
11971    }
11972
11973    #[tokio::test]
11974    async fn route_completions_engine_unavailable_maps_to_503() {
11975        let response = post_json(
11976            router_without_llm(),
11977            "/v1/completions",
11978            json!({
11979                "model": "stub-model",
11980                "prompt": "complete me"
11981            }),
11982        )
11983        .await;
11984        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11985        let body = response_json(response).await;
11986        assert_eq!(body["error"]["type"], "service_unavailable_error");
11987        assert_eq!(body["error"]["param"], Value::Null);
11988    }
11989
11990    #[tokio::test]
11991    async fn route_embeddings_engine_unavailable_maps_to_503() {
11992        let response = post_json(
11993            router_without_llm(),
11994            "/v1/embeddings",
11995            json!({
11996                "model": "embed-model",
11997                "input": "hello"
11998            }),
11999        )
12000        .await;
12001        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
12002        let body = response_json(response).await;
12003        assert_eq!(body["error"]["type"], "service_unavailable_error");
12004        assert_eq!(body["error"]["param"], Value::Null);
12005    }
12006
12007    #[tokio::test]
12008    async fn route_embeddings_contract_uses_stub_engine() {
12009        let response = post_json(
12010            router_with_stub_embed(),
12011            "/v1/embeddings",
12012            json!({
12013                "model": "stub-embed",
12014                "input": ["hi", "world"],
12015                "encoding_format": "float"
12016            }),
12017        )
12018        .await;
12019        assert_eq!(response.status(), AxumStatusCode::OK);
12020        let body = response_json(response).await;
12021        assert_eq!(body["object"], "list");
12022        assert_eq!(body["model"], "stub-embed");
12023        assert_eq!(body["usage"]["prompt_tokens"], 7);
12024        assert_eq!(body["usage"]["total_tokens"], 7);
12025
12026        let data = body["data"].as_array().expect("embedding data");
12027        assert_eq!(data.len(), 2, "body: {body}");
12028        assert_eq!(data[0]["object"], "embedding");
12029        assert_eq!(data[0]["index"], 0);
12030        assert_eq!(data[0]["embedding"].as_array().unwrap().len(), 3);
12031        assert_eq!(data[0]["embedding"][0].as_f64().unwrap(), 2.0);
12032        assert_eq!(data[1]["index"], 1);
12033        assert_eq!(data[1]["embedding"][0].as_f64().unwrap(), 5.0);
12034    }
12035
12036    #[tokio::test]
12037    async fn route_embeddings_public_alias_succeeds_and_unknown_alias_is_rejected() {
12038        let registry = ServedModelRegistry::try_new(
12039            "stub-embed",
12040            ServedModelKind::Embedding,
12041            vec!["public-embed".to_string()],
12042            vec![],
12043        )
12044        .unwrap();
12045        let server =
12046            AxumServer::from_embed(Arc::new(StubEmbed::new())).with_served_model_registry(registry);
12047        let accepted = post_json(
12048            server.build_router(),
12049            "/v1/embeddings",
12050            json!({"model": "public-embed", "input": "hello"}),
12051        )
12052        .await;
12053        assert_eq!(accepted.status(), AxumStatusCode::OK);
12054        assert_eq!(response_json(accepted).await["model"], "public-embed");
12055
12056        let rejected = post_json(
12057            server.build_router(),
12058            "/v1/embeddings",
12059            json!({"model": "stub-embed", "input": "hello"}),
12060        )
12061        .await;
12062        assert_eq!(rejected.status(), AxumStatusCode::BAD_REQUEST);
12063        let body = response_json(rejected).await;
12064        assert_eq!(body["error"]["type"], "invalid_request_error");
12065        assert_eq!(body["error"]["param"], "model");
12066    }
12067
12068    #[tokio::test]
12069    async fn route_embeddings_rejects_unsupported_encoding_format() {
12070        let response = post_json(
12071            router_with_stub_embed(),
12072            "/v1/embeddings",
12073            json!({
12074                "model": "stub-embed",
12075                "input": "hi",
12076                "encoding_format": "base64"
12077            }),
12078        )
12079        .await;
12080        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12081        let body = response_json(response).await;
12082        assert_eq!(body["error"]["type"], "invalid_request_error");
12083        assert_eq!(body["error"]["param"], "encoding_format");
12084    }
12085
12086    #[tokio::test]
12087    async fn route_embeddings_rejects_empty_input_with_field_param() {
12088        let response = post_json(
12089            router_with_stub_embed(),
12090            "/v1/embeddings",
12091            json!({
12092                "model": "stub-embed",
12093                "input": []
12094            }),
12095        )
12096        .await;
12097        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12098        let body = response_json(response).await;
12099        assert_eq!(body["error"]["type"], "invalid_request_error");
12100        assert_eq!(body["error"]["param"], "input");
12101    }
12102
12103    #[tokio::test]
12104    async fn route_embeddings_rejects_empty_item_with_field_param() {
12105        let response = post_json(
12106            router_with_stub_embed(),
12107            "/v1/embeddings",
12108            json!({
12109                "model": "stub-embed",
12110                "input": [{}]
12111            }),
12112        )
12113        .await;
12114        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12115        let body = response_json(response).await;
12116        assert_eq!(body["error"]["type"], "invalid_request_error");
12117        assert_eq!(body["error"]["param"], "input");
12118    }
12119
12120    #[tokio::test]
12121    async fn route_embeddings_invalid_json_maps_to_openai_error() {
12122        let response = post_raw_json(router_with_stub_embed(), "/v1/embeddings", "{").await;
12123        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12124        let body = response_json(response).await;
12125        assert_eq!(body["error"]["type"], "invalid_request_error");
12126        assert_eq!(body["error"]["param"], Value::Null);
12127        assert!(body["error"]["message"]
12128            .as_str()
12129            .unwrap()
12130            .contains("invalid embeddings request"));
12131    }
12132
12133    #[tokio::test]
12134    async fn route_transcriptions_engine_unavailable_maps_to_503() {
12135        let boundary = "ferrum-test-boundary";
12136        let body = concat!(
12137            "--ferrum-test-boundary\r\n",
12138            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
12139            "Content-Type: audio/wav\r\n",
12140            "\r\n",
12141            "RIFFtest\r\n",
12142            "--ferrum-test-boundary--\r\n"
12143        );
12144        let response = post_multipart(
12145            router_without_llm(),
12146            "/v1/audio/transcriptions",
12147            boundary,
12148            body,
12149        )
12150        .await;
12151        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
12152        let body = response_json(response).await;
12153        assert_eq!(body["error"]["type"], "service_unavailable_error");
12154        assert_eq!(body["error"]["param"], Value::Null);
12155    }
12156
12157    #[tokio::test]
12158    async fn route_transcriptions_contract_uses_stub_engine() {
12159        let boundary = "ferrum-test-boundary";
12160        let body = concat!(
12161            "--ferrum-test-boundary\r\n",
12162            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
12163            "Content-Type: audio/wav\r\n",
12164            "\r\n",
12165            "RIFFtest\r\n",
12166            "--ferrum-test-boundary\r\n",
12167            "Content-Disposition: form-data; name=\"language\"\r\n",
12168            "\r\n",
12169            "en\r\n",
12170            "--ferrum-test-boundary\r\n",
12171            "Content-Disposition: form-data; name=\"response_format\"\r\n",
12172            "\r\n",
12173            "json\r\n",
12174            "--ferrum-test-boundary--\r\n"
12175        );
12176        let response = post_multipart(
12177            router_with_stub_transcribe(),
12178            "/v1/audio/transcriptions",
12179            boundary,
12180            body,
12181        )
12182        .await;
12183        assert_eq!(response.status(), AxumStatusCode::OK);
12184        let body = response_json(response).await;
12185        assert_eq!(body["text"], "bytes:8:en");
12186    }
12187
12188    #[tokio::test]
12189    async fn route_transcriptions_rejects_unsupported_response_format() {
12190        let boundary = "ferrum-test-boundary";
12191        let body = concat!(
12192            "--ferrum-test-boundary\r\n",
12193            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
12194            "Content-Type: audio/wav\r\n",
12195            "\r\n",
12196            "RIFFtest\r\n",
12197            "--ferrum-test-boundary\r\n",
12198            "Content-Disposition: form-data; name=\"response_format\"\r\n",
12199            "\r\n",
12200            "text\r\n",
12201            "--ferrum-test-boundary--\r\n"
12202        );
12203        let response = post_multipart(
12204            router_with_stub_transcribe(),
12205            "/v1/audio/transcriptions",
12206            boundary,
12207            body,
12208        )
12209        .await;
12210        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12211        let body = response_json(response).await;
12212        assert_eq!(body["error"]["type"], "invalid_request_error");
12213        assert_eq!(body["error"]["param"], "response_format");
12214    }
12215
12216    #[tokio::test]
12217    async fn route_transcriptions_rejects_missing_file_with_field_param() {
12218        let boundary = "ferrum-test-boundary";
12219        let body = concat!(
12220            "--ferrum-test-boundary\r\n",
12221            "Content-Disposition: form-data; name=\"language\"\r\n",
12222            "\r\n",
12223            "en\r\n",
12224            "--ferrum-test-boundary--\r\n"
12225        );
12226        let response = post_multipart(
12227            router_with_stub_transcribe(),
12228            "/v1/audio/transcriptions",
12229            boundary,
12230            body,
12231        )
12232        .await;
12233        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12234        let body = response_json(response).await;
12235        assert_eq!(body["error"]["type"], "invalid_request_error");
12236        assert_eq!(body["error"]["param"], "file");
12237    }
12238
12239    #[tokio::test]
12240    async fn route_transcriptions_invalid_multipart_maps_to_openai_error() {
12241        let response = post_json(
12242            router_with_stub_transcribe(),
12243            "/v1/audio/transcriptions",
12244            json!({}),
12245        )
12246        .await;
12247        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12248        let body = response_json(response).await;
12249        assert_eq!(body["error"]["type"], "invalid_request_error");
12250        assert_eq!(body["error"]["param"], Value::Null);
12251        assert!(body["error"]["message"]
12252            .as_str()
12253            .unwrap()
12254            .contains("invalid transcriptions request"));
12255    }
12256
12257    #[tokio::test]
12258    async fn route_speech_engine_unavailable_maps_to_503() {
12259        let response = post_json(
12260            router_without_llm(),
12261            "/v1/audio/speech",
12262            json!({
12263                "model": "tts-model",
12264                "input": "hello",
12265                "voice": "default"
12266            }),
12267        )
12268        .await;
12269        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
12270        let body = response_json(response).await;
12271        assert_eq!(body["error"]["type"], "service_unavailable_error");
12272        assert_eq!(body["error"]["param"], Value::Null);
12273    }
12274
12275    #[tokio::test]
12276    async fn route_speech_contract_uses_stub_engine() {
12277        let response = post_json(
12278            router_with_stub_tts(),
12279            "/v1/audio/speech",
12280            json!({
12281                "model": "stub-tts",
12282                "input": "hello",
12283                "voice": "default",
12284                "response_format": "wav",
12285                "language": "english"
12286            }),
12287        )
12288        .await;
12289        assert_eq!(response.status(), AxumStatusCode::OK);
12290        assert_eq!(
12291            response.headers().get(header::CONTENT_TYPE).unwrap(),
12292            "audio/wav"
12293        );
12294        let body = response_bytes(response).await;
12295        assert!(body.len() > 44, "WAV should include header and PCM data");
12296        assert_eq!(&body[0..4], b"RIFF");
12297        assert_eq!(&body[8..12], b"WAVE");
12298    }
12299
12300    #[tokio::test]
12301    async fn route_speech_streaming_contract_uses_stub_engine() {
12302        let response = post_json(
12303            router_with_stub_tts(),
12304            "/v1/audio/speech",
12305            json!({
12306                "model": "stub-tts",
12307                "input": "hello",
12308                "voice": "default",
12309                "response_format": "wav",
12310                "stream": true
12311            }),
12312        )
12313        .await;
12314        assert_eq!(response.status(), AxumStatusCode::OK);
12315        assert_eq!(
12316            response.headers().get(header::CONTENT_TYPE).unwrap(),
12317            "audio/wav"
12318        );
12319        assert_eq!(
12320            response.headers().get(header::TRANSFER_ENCODING).unwrap(),
12321            "chunked"
12322        );
12323        let body = response_bytes(response).await;
12324        assert!(body.len() > 44, "streaming WAV should include audio bytes");
12325        assert_eq!(&body[0..4], b"RIFF");
12326        assert_eq!(&body[8..12], b"WAVE");
12327    }
12328
12329    #[tokio::test]
12330    async fn route_speech_pcm_response_format_returns_raw_pcm() {
12331        let response = post_json(
12332            router_with_stub_tts(),
12333            "/v1/audio/speech",
12334            json!({
12335                "model": "stub-tts",
12336                "input": "hello",
12337                "voice": "default",
12338                "response_format": "pcm"
12339            }),
12340        )
12341        .await;
12342        assert_eq!(response.status(), AxumStatusCode::OK);
12343        assert_eq!(
12344            response.headers().get(header::CONTENT_TYPE).unwrap(),
12345            "audio/pcm"
12346        );
12347        let body = response_bytes(response).await;
12348        assert_eq!(body.len(), 6, "three f32 samples should encode as s16le");
12349        assert_eq!(&body[0..2], &[0, 0]);
12350        assert_ne!(&body[0..4], b"RIFF");
12351    }
12352
12353    #[tokio::test]
12354    async fn route_speech_rejects_unsupported_response_format() {
12355        let response = post_json(
12356            router_with_stub_tts(),
12357            "/v1/audio/speech",
12358            json!({
12359                "model": "stub-tts",
12360                "input": "hello",
12361                "voice": "default",
12362                "response_format": "mp3"
12363            }),
12364        )
12365        .await;
12366        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12367        let body = response_json(response).await;
12368        assert_eq!(body["error"]["type"], "invalid_request_error");
12369        assert_eq!(body["error"]["param"], "response_format");
12370    }
12371
12372    #[tokio::test]
12373    async fn route_speech_invalid_json_maps_to_openai_error() {
12374        let response = post_raw_json(router_with_stub_tts(), "/v1/audio/speech", "{").await;
12375        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12376        let body = response_json(response).await;
12377        assert_eq!(body["error"]["type"], "invalid_request_error");
12378        assert_eq!(body["error"]["param"], Value::Null);
12379        assert!(body["error"]["message"]
12380            .as_str()
12381            .unwrap()
12382            .contains("invalid speech request"));
12383    }
12384
12385    #[tokio::test]
12386    async fn route_completions_generation_failure_maps_to_500() {
12387        let response = post_json(
12388            router_with_failing_llm(),
12389            "/v1/completions",
12390            json!({
12391                "model": "failing-model",
12392                "prompt": "complete me"
12393            }),
12394        )
12395        .await;
12396        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
12397        let body = response_json(response).await;
12398        assert_eq!(body["error"]["type"], "internal_server_error");
12399        assert!(body["error"]["message"]
12400            .as_str()
12401            .unwrap()
12402            .contains("stub generation failed"));
12403    }
12404
12405    #[tokio::test]
12406    async fn route_completions_stream_start_failure_maps_to_500_before_sse() {
12407        let response = post_json(
12408            router_with_failing_llm(),
12409            "/v1/completions",
12410            json!({
12411                "model": "failing-model",
12412                "prompt": "complete me",
12413                "stream": true
12414            }),
12415        )
12416        .await;
12417        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
12418        let body = response_json(response).await;
12419        assert_eq!(body["error"]["type"], "internal_server_error");
12420        assert!(body["error"]["message"]
12421            .as_str()
12422            .unwrap()
12423            .contains("stub stream failed"));
12424    }
12425
12426    #[tokio::test]
12427    async fn route_completions_stream_chunk_failure_emits_openai_error_event() {
12428        let response = post_json(
12429            router_with_stream_chunk_failing_llm(),
12430            "/v1/completions",
12431            json!({
12432                "model": "failing-model",
12433                "prompt": "complete me",
12434                "stream": true
12435            }),
12436        )
12437        .await;
12438        assert_eq!(response.status(), AxumStatusCode::OK);
12439        let body = response_text(response).await;
12440        assert_openai_stream_error(&body, "stub stream chunk failed");
12441    }
12442
12443    #[tokio::test]
12444    async fn route_completions_contract_uses_stub_engine() {
12445        let response = post_json(
12446            router_with_stub("done"),
12447            "/v1/completions",
12448            json!({
12449                "model": "stub-model",
12450                "prompt": "complete me",
12451                "max_tokens": 8,
12452                "temperature": 0.0
12453            }),
12454        )
12455        .await;
12456        assert_eq!(response.status(), AxumStatusCode::OK);
12457        let body = response_json(response).await;
12458        assert_eq!(body["object"], "text_completion");
12459        assert_eq!(body["choices"][0]["text"], "done");
12460        assert_eq!(body["usage"]["prompt_tokens"], 7);
12461        assert_eq!(body["usage"]["completion_tokens"], 2);
12462    }
12463
12464    #[tokio::test]
12465    async fn route_completions_public_alias_maps_to_internal_model() {
12466        let engine = Arc::new(CapturingLlm::new());
12467        let registry = ServedModelRegistry::try_new(
12468            "qwen3",
12469            ServedModelKind::Llm,
12470            vec!["served-alias".to_string()],
12471            vec![],
12472        )
12473        .unwrap();
12474        let router = AxumServer::from_llm(engine.clone())
12475            .with_served_model_registry(registry)
12476            .build_router();
12477        let response = post_json(
12478            router,
12479            "/v1/completions",
12480            json!({"model": "served-alias", "prompt": "complete me"}),
12481        )
12482        .await;
12483
12484        assert_eq!(response.status(), AxumStatusCode::OK);
12485        assert_eq!(response_json(response).await["model"], "served-alias");
12486        assert_eq!(engine.last_request().model_id, ModelId::new("qwen3"));
12487    }
12488
12489    #[tokio::test]
12490    async fn route_completions_streaming_contract_uses_stub_engine() {
12491        let response = post_json(
12492            router_with_stub("done"),
12493            "/v1/completions",
12494            json!({
12495                "model": "stub-model",
12496                "prompt": "complete me",
12497                "max_tokens": 8,
12498                "temperature": 0.0,
12499                "stream": true
12500            }),
12501        )
12502        .await;
12503        assert_eq!(response.status(), AxumStatusCode::OK);
12504        let body = response_text(response).await;
12505        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12506        assert!(
12507            body.contains("\"object\":\"text_completion\""),
12508            "missing completion chunk: {body}"
12509        );
12510        assert!(body.contains("\"text\":\"done\""), "missing text: {body}");
12511        assert!(
12512            body.contains("\"choices\":[],\"usage\""),
12513            "missing separate usage chunk: {body}"
12514        );
12515        assert!(
12516            body.contains("\"prompt_tokens\":5"),
12517            "stream usage should come from engine token usage: {body}"
12518        );
12519        assert!(
12520            body.contains("\"completion_tokens\":1"),
12521            "stream completion usage should come from engine token usage: {body}"
12522        );
12523    }
12524
12525    #[tokio::test]
12526    async fn route_completions_stream_waits_for_separate_final_usage_at_max_tokens() {
12527        let response = post_json(
12528            router_with_stub_separate_final_stream_chunk(&["do", "ne"]),
12529            "/v1/completions",
12530            json!({
12531                "model": "stub-model",
12532                "prompt": "complete me",
12533                "max_tokens": 2,
12534                "temperature": 0.0,
12535                "stream": true
12536            }),
12537        )
12538        .await;
12539        assert_eq!(response.status(), AxumStatusCode::OK);
12540        let body = response_text(response).await;
12541        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
12542        assert!(
12543            body.contains("\"text\":\"do\""),
12544            "missing first chunk: {body}"
12545        );
12546        assert!(
12547            body.contains("\"text\":\"ne\""),
12548            "missing second chunk: {body}"
12549        );
12550        assert!(
12551            body.contains("\"choices\":[],\"usage\""),
12552            "missing separate usage chunk from final engine chunk: {body}"
12553        );
12554        assert!(
12555            body.contains("\"prompt_tokens\":5"),
12556            "stream usage should come from engine final usage: {body}"
12557        );
12558    }
12559
12560    #[tokio::test]
12561    async fn route_completions_invalid_json_maps_to_openai_error() {
12562        let response = post_raw_json(router_with_stub("unused"), "/v1/completions", "{").await;
12563        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12564        let body = response_json(response).await;
12565        assert_eq!(body["error"]["type"], "invalid_request_error");
12566        assert_eq!(body["error"]["param"], Value::Null);
12567        assert!(body["error"]["message"]
12568            .as_str()
12569            .unwrap()
12570            .contains("invalid completions request"));
12571    }
12572
12573    #[tokio::test]
12574    async fn route_completions_rejects_unsupported_fields_explicitly() {
12575        for (extra, param) in [
12576            (json!({"n": 2}), "n"),
12577            (json!({"logprobs": 3}), "logprobs"),
12578            (json!({"logit_bias": {"42": 1.0}}), "logit_bias"),
12579        ] {
12580            let mut body = json!({
12581                "model": "stub-model",
12582                "prompt": "complete me"
12583            });
12584            body.as_object_mut()
12585                .expect("object")
12586                .extend(extra.as_object().expect("extra object").clone());
12587            let response = post_json(router_with_stub("unused"), "/v1/completions", body).await;
12588            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12589            let body = response_json(response).await;
12590            assert_eq!(body["error"]["type"], "invalid_request_error");
12591            assert_eq!(body["error"]["param"], param);
12592        }
12593    }
12594
12595    #[tokio::test]
12596    async fn streaming_completions_do_not_synthesize_whitespace_usage() {
12597        let response = post_json(
12598            router_with_stub_without_stream_usage("done"),
12599            "/v1/completions",
12600            json!({
12601                "model": "stub-model",
12602                "prompt": "one two three four",
12603                "stream": true
12604            }),
12605        )
12606        .await;
12607        assert_eq!(response.status(), AxumStatusCode::OK);
12608        let body = response_text(response).await;
12609        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12610        assert!(
12611            !body.contains("\"usage\":{\"prompt_tokens\""),
12612            "server must not synthesize whitespace-count completion usage: {body}"
12613        );
12614    }
12615
12616    #[tokio::test]
12617    async fn chat_rejects_n_not_one_with_openai_error_param() {
12618        let request = chat_request(json!({"n": 2}));
12619        let err = chat_completions_handler(
12620            State(state_with_stub("unused")),
12621            HeaderMap::new(),
12622            Ok(Json(request)),
12623        )
12624        .await
12625        .expect_err("n=2 should reject");
12626        let (status, body) = error_json(err).await;
12627        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
12628        assert_eq!(body["error"]["type"], "invalid_request_error");
12629        assert_eq!(body["error"]["param"], "n");
12630    }
12631
12632    #[tokio::test]
12633    async fn chat_rejects_logit_bias_and_logprobs_explicitly() {
12634        for (extra, param) in [
12635            (json!({"logit_bias": {"1": 100.0}}), "logit_bias"),
12636            (json!({"logprobs": true}), "logprobs"),
12637            (json!({"top_logprobs": 2}), "top_logprobs"),
12638        ] {
12639            let request = chat_request(extra);
12640            let err = chat_completions_handler(
12641                State(state_with_stub("unused")),
12642                HeaderMap::new(),
12643                Ok(Json(request)),
12644            )
12645            .await
12646            .expect_err("unsupported field should reject");
12647            let (status, body) = error_json(err).await;
12648            assert_eq!(status, AxumStatusCode::BAD_REQUEST);
12649            assert_eq!(body["error"]["param"], param);
12650            assert_eq!(body["error"]["type"], "invalid_request_error");
12651        }
12652    }
12653
12654    #[tokio::test]
12655    async fn chat_stream_options_include_usage_controls_stream_usage() {
12656        let request = chat_request(json!({
12657            "stream": true,
12658            "stream_options": {"include_usage": true}
12659        }));
12660        let response = chat_completions_handler(
12661            State(state_with_stub("ok")),
12662            HeaderMap::new(),
12663            Ok(Json(request)),
12664        )
12665        .await
12666        .expect("stream response");
12667        assert_eq!(response.status(), AxumStatusCode::OK);
12668        let body = response_text(response).await;
12669        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12670        assert!(
12671            body.contains("\"usage\"") && body.contains("\"completion_tokens\":1"),
12672            "include_usage=true should emit stream usage: {body}"
12673        );
12674        assert!(
12675            body.contains("\"choices\":[],\"usage\""),
12676            "include_usage=true should use a separate usage chunk: {body}"
12677        );
12678        assert!(
12679            body.contains("\"prompt_tokens\":5"),
12680            "stream usage should come from engine token usage: {body}"
12681        );
12682
12683        let request = chat_request(json!({"stream": true}));
12684        let response = chat_completions_handler(
12685            State(state_with_stub("ok")),
12686            HeaderMap::new(),
12687            Ok(Json(request)),
12688        )
12689        .await
12690        .expect("stream response");
12691        let body = response_text(response).await;
12692        assert!(
12693            !body.contains("\"usage\":{\"prompt_tokens\""),
12694            "stream usage should be omitted unless requested: {body}"
12695        );
12696    }
12697
12698    #[tokio::test]
12699    async fn streaming_chat_does_not_synthesize_whitespace_usage() {
12700        let response = post_json(
12701            router_with_stub_without_stream_usage("ok"),
12702            "/v1/chat/completions",
12703            json!({
12704                "model": "stub-model",
12705                "messages": [{"role": "user", "content": "one two three four"}],
12706                "stream": true,
12707                "stream_options": {"include_usage": true}
12708            }),
12709        )
12710        .await;
12711        assert_eq!(response.status(), AxumStatusCode::OK);
12712        let body = response_text(response).await;
12713        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12714        assert!(
12715            !body.contains("\"usage\":{\"prompt_tokens\""),
12716            "server must not synthesize whitespace-count usage when engine stream omits usage: {body}"
12717        );
12718    }
12719
12720    #[test]
12721    fn tool_requests_and_tool_messages_parse_into_structured_api_request() {
12722        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12723            "model": "qwen3",
12724            "messages": [
12725                {"role": "user", "content": "Use the weather tool."},
12726                {
12727                    "role": "assistant",
12728                    "content": null,
12729                    "tool_calls": [{
12730                        "id": "call_1",
12731                        "type": "function",
12732                        "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
12733                    }]
12734                },
12735                {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
12736            ],
12737            "tools": [{
12738                "type": "function",
12739                "function": {
12740                    "name": "weather",
12741                    "description": "Get weather",
12742                    "parameters": {
12743                        "type": "object",
12744                        "properties": {"city": {"type": "string"}},
12745                        "required": ["city"]
12746                    }
12747                }
12748            }],
12749            "tool_choice": "auto"
12750        }))
12751        .expect("tool request parses");
12752
12753        validate_chat_request(&request).expect("tool request validates");
12754        let internal = convert_chat_request(&request).expect("convert");
12755        assert!(internal.prompt.contains("\"tools\":[{"));
12756        assert!(internal.prompt.contains("\"type\":\"function\""));
12757        assert!(internal.prompt.contains("\"name\":\"weather\""));
12758        assert!(internal.prompt.contains("<|im_start|>assistant\n{"));
12759        assert!(internal.prompt.contains("\"tool_calls\":[{"));
12760        assert!(internal.prompt.contains("\"id\":\"call_1\""));
12761        assert!(internal
12762            .prompt
12763            .contains("<|im_start|>tool\nsunny<|im_end|>"));
12764        assert_eq!(
12765            internal.metadata["openai_tools"][0]["function"]["name"],
12766            "weather"
12767        );
12768        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12769        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12770            panic!("expected structured chat api_request");
12771        };
12772        assert_eq!(api.messages.len(), 3);
12773        assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Tool);
12774        assert_eq!(api.messages[2].content, "sunny");
12775        assert_eq!(api.messages[2].tool_call_id.as_deref(), Some("call_1"));
12776        assert_eq!(api.tools[0].function.name, "weather");
12777        assert_eq!(
12778            api.tool_choice,
12779            Some(ferrum_types::ApiToolChoice::Mode("auto".into()))
12780        );
12781        assert_eq!(
12782            api.messages[1].tool_calls[0].function.arguments,
12783            "{\"city\":\"Paris\"}"
12784        );
12785    }
12786
12787    #[test]
12788    fn omitted_tool_choice_defaults_to_auto_when_tools_are_present() {
12789        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12790            "model": "served-alias",
12791            "messages": [{"role": "user", "content": "Use the weather tool."}],
12792            "tools": [{
12793                "type": "function",
12794                "function": {
12795                    "name": "weather",
12796                    "description": "Get weather",
12797                    "parameters": {
12798                        "type": "object",
12799                        "properties": {"city": {"type": "string"}},
12800                        "required": ["city"]
12801                    }
12802                }
12803            }]
12804        }))
12805        .expect("tool request parses");
12806
12807        validate_chat_request(&request).expect("tool request validates");
12808        let internal = convert_chat_request(&request).expect("convert");
12809        assert!(internal.prompt.contains("\"tools\":[{"));
12810        assert!(internal.prompt.contains("\"tool_choice\":\"auto\""));
12811        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12812        let initial_forbidden = internal.metadata[INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY]
12813            .as_array()
12814            .expect("initial forbidden token list");
12815        assert_eq!(initial_forbidden, &[serde_json::json!(THINK_END_TAG)]);
12816        assert_eq!(
12817            internal.sampling_params.response_format,
12818            ferrum_types::ResponseFormat::Text,
12819            "auto tool choice must preserve native model selection instead of forcing arguments JSON",
12820        );
12821        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12822            panic!("expected structured chat api_request");
12823        };
12824        assert_eq!(
12825            api.tool_choice,
12826            Some(ferrum_types::ApiToolChoice::Mode("auto".into()))
12827        );
12828    }
12829
12830    #[test]
12831    fn omitted_tool_choice_uses_native_template_protocol_without_hard_schema() {
12832        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12833            "model": "served-alias",
12834            "messages": [{"role": "user", "content": "北京现在天气怎么样?用摄氏度。"}],
12835            "tools": [{
12836                "type": "function",
12837                "function": {
12838                    "name": "get_weather",
12839                    "description": "查询指定城市的当前天气",
12840                    "parameters": {
12841                        "type": "object",
12842                        "properties": {
12843                            "city": {"type": "string"},
12844                            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
12845                        },
12846                        "required": ["city"]
12847                    }
12848                }
12849            }]
12850        }))
12851        .expect("tool request parses");
12852        let template = ModelChatTemplate::new(
12853            "{% 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 %}",
12854            "function-parameter-xml-template",
12855        );
12856
12857        validate_chat_request(&request).expect("tool request validates");
12858        let internal =
12859            convert_chat_request_with_template_model(&request, "served-alias", Some(&template))
12860                .expect("convert");
12861        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12862        assert_eq!(
12863            internal.sampling_params.response_format,
12864            ferrum_types::ResponseFormat::Text,
12865        );
12866        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request else {
12867            panic!("expected chat API request");
12868        };
12869        assert_eq!(
12870            api.tool_call_protocol,
12871            ferrum_types::ApiToolCallProtocol::FunctionParameterXml,
12872        );
12873    }
12874
12875    #[test]
12876    fn tool_schema_response_format_bounds_unconstrained_strings() {
12877        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12878            "model": "served-alias",
12879            "messages": [{"role": "user", "content": "Use the selected tool."}],
12880            "tools": [{
12881                "type": "function",
12882                "function": {
12883                    "name": "get_weather",
12884                    "parameters": {
12885                        "type": "object",
12886                        "properties": {
12887                            "city": {"type": "string"},
12888                            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
12889                        },
12890                        "required": ["city"]
12891                    }
12892                }
12893            }],
12894            "tool_choice": {
12895                "type": "function",
12896                "function": {"name": "get_weather"}
12897            }
12898        }))
12899        .expect("tool request parses");
12900
12901        validate_chat_request(&request).expect("tool request validates");
12902        let internal = convert_chat_request(&request).expect("convert");
12903        match internal.sampling_params.response_format {
12904            ferrum_types::ResponseFormat::JsonSchema(ref schema) => {
12905                let value: serde_json::Value =
12906                    serde_json::from_str(schema).expect("schema should be JSON");
12907                assert_eq!(
12908                    value["properties"]["city"]["maxLength"],
12909                    DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH
12910                );
12911                assert_eq!(
12912                    value["properties"]["unit"]["enum"],
12913                    json!(["celsius", "fahrenheit"])
12914                );
12915                assert!(
12916                    value["properties"]["unit"]["maxLength"].is_null(),
12917                    "enum string should remain finite via enum instead of maxLength: {value}"
12918                );
12919            }
12920            ref other => panic!("expected forced tool json schema, got {other:?}"),
12921        }
12922    }
12923
12924    #[test]
12925    fn forced_native_tool_choice_preserves_xml_framing_with_a_tool_only_grammar() {
12926        let template = ModelChatTemplate::new(
12927            "{% 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 %}",
12928            "native-tool-fixture",
12929        );
12930        for choice in [
12931            json!({"type":"function","function":{"name":"calc"}}),
12932            json!("required"),
12933        ] {
12934            let request: ChatCompletionsRequest = serde_json::from_value(json!({
12935                "model":"served-alias","messages":[{"role":"user","content":"Use the selected tool."}],
12936                "tools":[
12937                    {"type":"function","function":{"name":"calc","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}},
12938                    {"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}
12939                ],
12940                "tool_choice":choice
12941            })).unwrap();
12942            validate_chat_request(&request).unwrap();
12943            let internal =
12944                convert_chat_request_with_template_model(&request, "served-alias", Some(&template))
12945                    .unwrap();
12946            assert!(internal.requires_structured_output());
12947            assert_eq!(
12948                internal.sampling_params.response_format,
12949                ferrum_types::ResponseFormat::Text
12950            );
12951            assert!(internal.prompt.contains("<function=name>"));
12952            let Some(ferrum_types::ApiRequest::Chat(chat)) = &internal.api_request else {
12953                panic!("chat contract")
12954            };
12955            assert!(chat.requires_native_tool_call());
12956            assert!(chat.allows_tool_name("calc"));
12957            assert_eq!(chat.allows_tool_name("lookup"), choice == json!("required"));
12958            assert_eq!(
12959                internal.sampling_params.structured_output_start,
12960                StructuredOutputStart::Immediate
12961            );
12962        }
12963    }
12964
12965    #[test]
12966    fn harmony_named_tool_choice_preserves_native_protocol_envelope() {
12967        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12968            "model": "gpt-oss-20b-mxfp4",
12969            "messages": [{
12970                "role": "user",
12971                "content": "Call get_weather exactly once with city set to Paris."
12972            }],
12973            "tools": [{
12974                "type": "function",
12975                "function": {
12976                    "name": "get_weather",
12977                    "parameters": {
12978                        "type": "object",
12979                        "properties": {"city": {"type": "string"}},
12980                        "required": ["city"],
12981                        "additionalProperties": false
12982                    }
12983                }
12984            }],
12985            "tool_choice": {
12986                "type": "function",
12987                "function": {"name": "get_weather"}
12988            }
12989        }))
12990        .expect("Harmony tool request parses");
12991        let mut template = ModelChatTemplate::new(
12992            "{% 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 %}",
12993            "harmony-tool-template",
12994        );
12995        template.output_protocol = ModelOutputProtocol::HarmonyGptOss;
12996
12997        validate_chat_request(&request).expect("Harmony tool request validates");
12998        let internal =
12999            convert_chat_request_with_template_model(&request, "gpt-oss-20b", Some(&template))
13000                .expect("convert Harmony tool request");
13001
13002        assert!(internal.prompt.ends_with("<|start|>assistant"));
13003        assert_eq!(
13004            internal.sampling_params.model_output_protocol,
13005            ModelOutputProtocol::HarmonyGptOss
13006        );
13007        assert_eq!(
13008            internal.sampling_params.response_format,
13009            ferrum_types::ResponseFormat::Text,
13010            "Harmony must generate its channel/message/call envelope before tool arguments"
13011        );
13012        assert_eq!(
13013            internal.metadata[INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY],
13014            json!([]),
13015            "Harmony declares no think delimiter and must not receive the generic structured-call mask"
13016        );
13017        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request else {
13018            panic!("expected chat API request");
13019        };
13020        assert_eq!(
13021            api.tool_choice,
13022            Some(ferrum_types::ApiToolChoice::Function {
13023                tool_type: "function".to_string(),
13024                function: ferrum_types::ApiToolChoiceFunction {
13025                    name: "get_weather".to_string(),
13026                },
13027            })
13028        );
13029    }
13030
13031    #[test]
13032    fn required_tool_choice_uses_tool_schema_response_format_without_extra_prompt_instruction() {
13033        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13034            "model": "served-alias",
13035            "messages": [{"role": "user", "content": "Call capture_quality_marker."}],
13036            "tools": [{
13037                "type": "function",
13038                "function": {
13039                    "name": "capture_quality_marker",
13040                    "description": "Record one marker.",
13041                    "parameters": {
13042                        "type": "object",
13043                        "properties": {
13044                            "marker": {"type": "string", "enum": ["ferrum0401"]},
13045                            "checksum": {"type": "string", "enum": ["S0004"]}
13046                        },
13047                        "required": ["marker", "checksum"]
13048                    }
13049                }
13050            }],
13051            "tool_choice": "required"
13052        }))
13053        .expect("tool request parses");
13054
13055        validate_chat_request(&request).expect("tool request validates");
13056        let internal = convert_chat_request(&request).expect("convert");
13057
13058        assert!(
13059            !internal.prompt.contains(
13060                "Output only a single JSON object containing the selected function arguments"
13061            ),
13062            "{}",
13063            internal.prompt
13064        );
13065        assert!(
13066            internal.prompt.contains("\"tool_choice\":\"required\""),
13067            "{}",
13068            internal.prompt
13069        );
13070        assert_eq!(internal.metadata["openai_tool_choice"], "required");
13071        match internal.sampling_params.response_format {
13072            ferrum_types::ResponseFormat::JsonSchema(ref schema) => {
13073                assert!(schema.contains(r#""enum":["ferrum0401"]"#), "{schema}");
13074                assert!(schema.contains(r#""enum":["S0004"]"#), "{schema}");
13075            }
13076            ref other => panic!("expected forced tool json schema, got {other:?}"),
13077        }
13078    }
13079
13080    #[test]
13081    fn required_tool_choice_suppresses_conflicting_response_format_instruction() {
13082        let request: ChatCompletionsRequest =
13083            serde_json::from_value(required_tool_with_strict_response_format_request(false))
13084                .expect("request parses");
13085
13086        validate_chat_request(&request).expect("request validates");
13087        let internal = convert_chat_request(&request).expect("convert");
13088
13089        assert!(
13090            !internal.prompt.contains("response_format requires"),
13091            "required tool output must not receive a conflicting content-schema instruction: {}",
13092            internal.prompt
13093        );
13094        let ferrum_types::ResponseFormat::JsonSchema(schema) =
13095            internal.sampling_params.response_format
13096        else {
13097            panic!("single required tool must use its argument schema");
13098        };
13099        let schema: Value = serde_json::from_str(&schema).expect("tool schema JSON");
13100        assert!(schema["properties"].get("city").is_some(), "{schema}");
13101        assert!(schema["properties"].get("answer").is_none(), "{schema}");
13102    }
13103
13104    #[test]
13105    fn required_multiple_tools_do_not_force_the_first_tool_schema() {
13106        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13107            "model": "stub-model",
13108            "messages": [{"role": "user", "content": "Use the appropriate tool."}],
13109            "tools": [
13110                {
13111                    "type": "function",
13112                    "function": {
13113                        "name": "weather",
13114                        "parameters": {
13115                            "type": "object",
13116                            "properties": {"city": {"type": "string"}},
13117                            "required": ["city"]
13118                        }
13119                    }
13120                },
13121                {
13122                    "type": "function",
13123                    "function": {
13124                        "name": "calendar",
13125                        "parameters": {
13126                            "type": "object",
13127                            "properties": {"date": {"type": "string"}},
13128                            "required": ["date"]
13129                        }
13130                    }
13131                }
13132            ],
13133            "tool_choice": "required"
13134        }))
13135        .expect("request parses");
13136
13137        validate_chat_request(&request).expect("request validates");
13138        let internal = convert_chat_request(&request).expect("convert");
13139        assert_eq!(
13140            internal.sampling_params.response_format,
13141            ferrum_types::ResponseFormat::Text,
13142            "required permits either declared tool, so guided decoding cannot bind the first tool's arguments"
13143        );
13144    }
13145
13146    #[test]
13147    fn omitted_single_unrelated_tool_keeps_text_response_format() {
13148        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13149            "model": "served-alias",
13150            "messages": [{"role": "user", "content": "讲一个短笑话。"}],
13151            "tools": [{
13152                "type": "function",
13153                "function": {
13154                    "name": "get_weather",
13155                    "description": "查询指定城市的当前天气",
13156                    "parameters": {
13157                        "type": "object",
13158                        "properties": {"city": {"type": "string"}},
13159                        "required": ["city"]
13160                    }
13161                }
13162            }]
13163        }))
13164        .expect("tool request parses");
13165
13166        validate_chat_request(&request).expect("tool request validates");
13167        let internal = convert_chat_request(&request).expect("convert");
13168        assert_eq!(
13169            internal.sampling_params.response_format,
13170            ferrum_types::ResponseFormat::Text
13171        );
13172    }
13173
13174    #[test]
13175    fn tool_choice_none_omits_tools_from_model_template_prompt() {
13176        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13177            "model": "served-alias",
13178            "messages": [
13179                {"role": "user", "content": "Use the weather tool if needed."},
13180                {
13181                    "role": "assistant",
13182                    "content": null,
13183                    "tool_calls": [{
13184                        "id": "call_1",
13185                        "type": "function",
13186                        "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
13187                    }]
13188                },
13189                {"role": "tool", "tool_call_id": "call_1", "content": "{\"temp\":22}"}
13190            ],
13191            "tools": [{
13192                "type": "function",
13193                "function": {"name": "weather", "parameters": {"type": "object"}}
13194            }],
13195            "tool_choice": "none"
13196        }))
13197        .expect("tool_choice none request parses");
13198        let template = ModelChatTemplate::new(
13199            "{% 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 %}",
13200            "tool-choice-none-template",
13201        );
13202
13203        validate_chat_request(&request).expect("tool_choice none request validates");
13204        let internal = convert_chat_request_with_template_model(
13205            &request,
13206            "served-template-model",
13207            Some(&template),
13208        )
13209        .expect("convert");
13210        assert!(
13211            !internal.prompt.contains("<tools>"),
13212            "tool_choice none must not expose tools to the model template: {}",
13213            internal.prompt
13214        );
13215        assert!(internal.prompt.contains("[tool]"), "{}", internal.prompt);
13216        assert_eq!(
13217            internal.metadata["openai_tools"][0]["function"]["name"],
13218            "weather"
13219        );
13220        assert_eq!(internal.metadata["openai_tool_choice"], "none");
13221        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13222            panic!("expected structured chat api_request");
13223        };
13224        assert_eq!(api.tools[0].function.name, "weather");
13225        assert_eq!(
13226            api.tool_choice,
13227            Some(ferrum_types::ApiToolChoice::Mode("none".into()))
13228        );
13229    }
13230
13231    #[test]
13232    fn specific_tool_choice_parses_into_structured_api_request() {
13233        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13234            "model": "qwen3",
13235            "messages": [{"role": "user", "content": "Use the selected tool."}],
13236            "tools": [
13237                {
13238                    "type": "function",
13239                    "function": {"name": "weather", "parameters": {"type": "object"}}
13240                },
13241                {
13242                    "type": "function",
13243                    "function": {"name": "calendar", "parameters": {"type": "object"}}
13244                }
13245            ],
13246            "tool_choice": {
13247                "type": "function",
13248                "function": {"name": "weather"}
13249            }
13250        }))
13251        .expect("specific tool_choice request parses");
13252
13253        validate_chat_request(&request).expect("specific tool_choice validates");
13254        let internal = convert_chat_request(&request).expect("convert");
13255        assert!(internal.prompt.contains("\"tool_choice\":{"));
13256        assert!(internal.prompt.contains("\"name\":\"weather\""));
13257        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13258            panic!("expected structured chat api_request");
13259        };
13260        assert_eq!(
13261            api.tool_choice,
13262            Some(ferrum_types::ApiToolChoice::Function {
13263                tool_type: "function".to_string(),
13264                function: ferrum_types::ApiToolChoiceFunction {
13265                    name: "weather".to_string()
13266                },
13267            })
13268        );
13269
13270        let invalid: ChatCompletionsRequest = serde_json::from_value(json!({
13271            "model": "qwen3",
13272            "messages": [{"role": "user", "content": "Use the selected tool."}],
13273            "tools": [{
13274                "type": "function",
13275                "function": {"name": "weather", "parameters": {"type": "object"}}
13276            }],
13277            "tool_choice": {
13278                "type": "function",
13279                "function": {"name": "calendar"}
13280            }
13281        }))
13282        .expect("invalid specific tool_choice request parses");
13283        let err = validate_chat_request(&invalid).expect_err("undeclared tool should reject");
13284        match err {
13285            ServerError::InvalidRequest { param, .. } => {
13286                assert_eq!(param.as_deref(), Some("tool_choice"));
13287            }
13288            other => panic!("expected invalid_request_error for tool_choice, got {other:?}"),
13289        }
13290    }
13291
13292    #[test]
13293    fn legacy_function_role_messages_parse_into_structured_api_request() {
13294        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13295            "model": "mystery-model",
13296            "messages": [
13297                {"role": "user", "content": "Call weather."},
13298                {
13299                    "role": "assistant",
13300                    "content": null,
13301                    "function_call": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
13302                },
13303                {"role": "function", "name": "weather", "content": "{\"forecast\":\"sunny\"}"}
13304            ],
13305            "functions": [{
13306                "name": "weather",
13307                "parameters": {
13308                    "type": "object",
13309                    "properties": {"city": {"type": "string"}},
13310                    "required": ["city"]
13311                }
13312            }],
13313            "function_call": "auto"
13314        }))
13315        .expect("legacy function request parses");
13316
13317        validate_chat_request(&request).expect("legacy function request validates");
13318        let internal = convert_chat_request(&request).expect("convert");
13319        assert!(
13320            internal
13321                .prompt
13322                .contains("<|function|>\n{\"forecast\":\"sunny\"}</s>"),
13323            "legacy function role should be preserved in fallback template: {}",
13324            internal.prompt
13325        );
13326        assert_eq!(
13327            internal.metadata["openai_legacy_functions"][0]["name"],
13328            "weather"
13329        );
13330        assert_eq!(internal.metadata["openai_legacy_function_call"], "auto");
13331        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13332            panic!("expected structured chat api_request");
13333        };
13334        assert_eq!(api.messages.len(), 3);
13335        assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Function);
13336        assert_eq!(api.messages[2].name.as_deref(), Some("weather"));
13337        assert_eq!(
13338            api.messages[1]
13339                .function_call
13340                .as_ref()
13341                .map(|call| call.name.as_str()),
13342            Some("weather")
13343        );
13344        assert_eq!(api.legacy_functions[0].name, "weather");
13345        assert_eq!(
13346            api.legacy_function_call,
13347            Some(ferrum_types::ApiFunctionCallChoice::Mode("auto".into()))
13348        );
13349    }
13350
13351    #[test]
13352    fn specific_legacy_function_call_parses_into_structured_api_request() {
13353        let request: ChatCompletionsRequest = serde_json::from_value(json!({
13354            "model": "mystery-model",
13355            "messages": [{"role": "user", "content": "Use the selected function."}],
13356            "functions": [
13357                {"name": "weather", "parameters": {"type": "object"}},
13358                {"name": "calendar", "parameters": {"type": "object"}}
13359            ],
13360            "function_call": {"name": "weather"}
13361        }))
13362        .expect("specific function_call request parses");
13363
13364        validate_chat_request(&request).expect("specific function_call validates");
13365        let internal = convert_chat_request(&request).expect("convert");
13366        assert!(internal.prompt.contains("\"function_call\":{"));
13367        assert!(internal.prompt.contains("\"name\":\"weather\""));
13368        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13369            panic!("expected structured chat api_request");
13370        };
13371        assert_eq!(
13372            api.legacy_function_call,
13373            Some(ferrum_types::ApiFunctionCallChoice::Function {
13374                name: "weather".to_string(),
13375            })
13376        );
13377
13378        let invalid: ChatCompletionsRequest = serde_json::from_value(json!({
13379            "model": "mystery-model",
13380            "messages": [{"role": "user", "content": "Use the selected function."}],
13381            "functions": [{"name": "weather", "parameters": {"type": "object"}}],
13382            "function_call": {"name": "calendar"}
13383        }))
13384        .expect("invalid specific function_call request parses");
13385        let err = validate_chat_request(&invalid).expect_err("undeclared function should reject");
13386        match err {
13387            ServerError::InvalidRequest { param, .. } => {
13388                assert_eq!(param.as_deref(), Some("function_call"));
13389            }
13390            other => panic!("expected invalid_request_error for function_call, got {other:?}"),
13391        }
13392    }
13393
13394    #[test]
13395    fn stream_text_delta_handles_unicode_boundaries() {
13396        let mut sent_len = 0usize;
13397        assert_eq!(stream_text_delta("你好", &mut sent_len), "你好");
13398        assert_eq!(sent_len, "你好".len());
13399        assert_eq!(stream_text_delta("你好世界", &mut sent_len), "世界");
13400        assert_eq!(sent_len, "你好世界".len());
13401    }
13402
13403    #[test]
13404    fn stream_text_delta_recovers_from_non_boundary_offset() {
13405        let mut sent_len = 1usize;
13406        assert_eq!(stream_text_delta("你好", &mut sent_len), "");
13407        assert_eq!(sent_len, "你好".len());
13408    }
13409
13410    #[test]
13411    fn assistant_tool_call_serializes_openai_shape() {
13412        let message = ChatMessage {
13413            role: MessageRole::Assistant,
13414            content: String::new(),
13415            reasoning: None,
13416            name: None,
13417            tool_calls: Some(vec![ChatToolCall {
13418                index: None,
13419                id: "call_1".to_string(),
13420                tool_type: "function".to_string(),
13421                function: ChatFunctionCall {
13422                    name: "weather".to_string(),
13423                    arguments: "{\"city\":\"Paris\"}".to_string(),
13424                },
13425            }]),
13426            tool_call_id: None,
13427            function_call: None,
13428        };
13429        let value = serde_json::to_value(message).expect("serialize");
13430        assert_eq!(value["role"], "assistant");
13431        assert_eq!(value["tool_calls"][0]["type"], "function");
13432        assert_eq!(value["tool_calls"][0]["function"]["name"], "weather");
13433    }
13434
13435    #[test]
13436    fn unsupported_multimodal_content_is_not_silently_dropped() {
13437        let err = serde_json::from_value::<ChatCompletionsRequest>(json!({
13438            "model": "stub-model",
13439            "messages": [{
13440                "role": "user",
13441                "content": [
13442                    {"type": "text", "text": "describe this"},
13443                    {"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}}
13444                ]
13445            }]
13446        }))
13447        .expect_err("unsupported content part should fail parsing");
13448        assert!(
13449            err.to_string()
13450                .contains("unsupported message content part type"),
13451            "unexpected error: {err}"
13452        );
13453    }
13454
13455    #[tokio::test]
13456    async fn completions_endpoint_uses_stub_engine() {
13457        let request = CompletionsRequest {
13458            model: "stub-model".to_string(),
13459            prompt: CompletionPrompt::Text("complete me".to_string()),
13460            max_tokens: Some(8),
13461            temperature: Some(0.0),
13462            top_p: None,
13463            n: None,
13464            stream: None,
13465            stop: None,
13466            logprobs: None,
13467            logit_bias: None,
13468        };
13469        let response = completions_handler(State(state_with_stub("done")), Ok(Json(request)))
13470            .await
13471            .expect("completion response");
13472        assert_eq!(response.status(), AxumStatusCode::OK);
13473        let body = response_json(response).await;
13474        assert_eq!(body["object"], "text_completion");
13475        assert_eq!(body["choices"][0]["text"], "done");
13476        assert_eq!(body["usage"]["prompt_tokens"], 7);
13477        assert_eq!(body["usage"]["completion_tokens"], 2);
13478    }
13479
13480    #[tokio::test]
13481    async fn route_completions_rejects_non_string_prompt_with_field_param() {
13482        for prompt in [
13483            json!(["a", "b"]),
13484            json!({"text": "complete me"}),
13485            Value::Null,
13486        ] {
13487            let response = post_json(
13488                router_with_stub("unused"),
13489                "/v1/completions",
13490                json!({
13491                    "model": "stub-model",
13492                    "prompt": prompt
13493                }),
13494            )
13495            .await;
13496            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
13497            let body = response_json(response).await;
13498            assert_eq!(body["error"]["type"], "invalid_request_error");
13499            assert_eq!(body["error"]["param"], "prompt");
13500        }
13501
13502        let response = post_json(
13503            router_with_stub("unused"),
13504            "/v1/completions",
13505            json!({"model": "stub-model"}),
13506        )
13507        .await;
13508        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
13509        let body = response_json(response).await;
13510        assert_eq!(body["error"]["type"], "invalid_request_error");
13511        assert_eq!(body["error"]["param"], "prompt");
13512    }
13513
13514    #[tokio::test]
13515    async fn stream_options_without_stream_is_invalid() {
13516        let request = chat_request(json!({"stream_options": {"include_usage": true}}));
13517        let err = chat_completions_handler(
13518            State(state_with_stub("unused")),
13519            HeaderMap::new(),
13520            Ok(Json(request)),
13521        )
13522        .await
13523        .expect_err("stream_options without stream should reject");
13524        let (status, body) = error_json(err).await;
13525        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
13526        assert_eq!(body["error"]["param"], "stream_options");
13527        assert_eq!(body["error"]["type"], "invalid_request_error");
13528    }
13529
13530    #[tokio::test]
13531    async fn unknown_stream_option_is_rejected_instead_of_ignored() {
13532        let response = post_json(
13533            router_with_stub("unused"),
13534            "/v1/chat/completions",
13535            json!({
13536                "model": "stub-model",
13537                "messages": [{"role": "user", "content": "hello"}],
13538                "stream": true,
13539                "stream_options": {"continuous_usage_stats": true}
13540            }),
13541        )
13542        .await;
13543
13544        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
13545        let body = response_json(response).await;
13546        assert_eq!(body["error"]["type"], "invalid_request_error");
13547        assert!(
13548            body["error"]["message"]
13549                .as_str()
13550                .unwrap_or_default()
13551                .contains("invalid chat completions request"),
13552            "body: {body}"
13553        );
13554    }
13555
13556    #[tokio::test]
13557    async fn json_object_rejects_markdown_fence_instead_of_repairing() {
13558        let request = chat_request(json!({
13559            "response_format": {"type": "json_object"}
13560        }));
13561        let err = chat_completions_handler(
13562            State(state_with_stub("```json\n{\"answer\":\"yes\"}\n```")),
13563            HeaderMap::new(),
13564            Ok(Json(request)),
13565        )
13566        .await
13567        .expect_err("fenced json_object must fail");
13568        let (status, body) = error_json(err).await;
13569        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
13570        assert_eq!(body["error"]["type"], "internal_server_error");
13571        assert!(body["error"]["message"]
13572            .as_str()
13573            .unwrap_or_default()
13574            .contains("response_format.json_object: invalid JSON"));
13575    }
13576
13577    #[tokio::test]
13578    async fn streaming_json_object_buffers_thinking_and_emits_clean_json_content() {
13579        let response = post_json(
13580            router_with_stub_stream_chunks(&[
13581                "<think>\n好的,我需要输出 JSON。",
13582                "\n</think>\n\n",
13583                "{\"name\":\"李四\",\"age\":30}",
13584            ]),
13585            "/v1/chat/completions",
13586            json!({
13587                "model": "stub-model",
13588                "messages": [{"role": "user", "content": "输出JSON(name,age):李四,30岁"}],
13589                "stream": true,
13590                "response_format": {"type": "json_object"}
13591            }),
13592        )
13593        .await;
13594        assert_eq!(response.status(), AxumStatusCode::OK);
13595        let body = response_text(response).await;
13596        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
13597        assert!(
13598            body.contains(r#""content":"{\"name\":\"李四\",\"age\":30}""#),
13599            "stream should emit clean JSON content: {body}"
13600        );
13601        assert!(
13602            body.contains(r#""reasoning":"\n好的,我需要输出 JSON。\n""#),
13603            "stream should keep thinking in reasoning field: {body}"
13604        );
13605        assert!(
13606            !body.contains(r#""content":"<think"#)
13607                && !body.contains(r#""content":"好的"#)
13608                && !body.contains(r#""content":"我需要"#),
13609            "thinking text must not leak as streamed content: {body}"
13610        );
13611    }
13612
13613    fn prompt_opened_literal_json_template() -> ModelChatTemplate {
13614        let template = ModelChatTemplate::new(
13615            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant><think>{% endif %}",
13616            "prompt-opened-text-test",
13617        );
13618        assert_eq!(template.output_protocol, ModelOutputProtocol::Text);
13619        assert_eq!(
13620            template.reasoning_protocol,
13621            ModelReasoningProtocol::PromptOpened
13622        );
13623        let request = chat_request(json!({"response_format": {"type": "json_object"}}));
13624        let internal =
13625            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13626                .expect("convert prompt-opened Text request");
13627        assert!(internal.prompt.ends_with("<think>"));
13628        template
13629    }
13630
13631    #[tokio::test]
13632    async fn json_object_preserves_literal_think_tags_after_prompt_opened_reasoning_sync() {
13633        let response = post_json(
13634            router_with_stub_and_template(
13635                "reason</think>\n{\"text\":\"<think>literal</think>\"}",
13636                prompt_opened_literal_json_template(),
13637            ),
13638            "/v1/chat/completions",
13639            json!({
13640                "model": "stub-model",
13641                "messages": [{"role": "user", "content": "Return a JSON object."}],
13642                "response_format": {"type": "json_object"}
13643            }),
13644        )
13645        .await;
13646        let status = response.status();
13647        let body = response_json(response).await;
13648        assert_eq!(status, AxumStatusCode::OK, "{body}");
13649        assert!(body.get("error").is_none(), "{body}");
13650        let message = &body["choices"][0]["message"];
13651        assert_eq!(message["content"], r#"{"text":"<think>literal</think>"}"#);
13652        assert_eq!(message["reasoning"], "reason");
13653        assert_eq!(body["choices"][0]["finish_reason"], "stop");
13654    }
13655
13656    #[tokio::test]
13657    async fn json_object_preserves_literal_think_tags_after_prompt_opened_reasoning_sse() {
13658        for chunks in [
13659            vec!["reason</think>\n{\"text\":\"<think>literal</think>\"}"],
13660            vec![
13661                "reason</thi",
13662                "nk>\n{\"text\":\"<thi",
13663                "nk>literal</thi",
13664                "nk>\"}",
13665            ],
13666        ] {
13667            let router = AxumServer::from_llm(Arc::new(StubLlm::with_stream_chunks(&chunks)))
13668                .with_prompt_template(Some(prompt_opened_literal_json_template()))
13669                .build_router();
13670            let response = post_json(
13671                router,
13672                "/v1/chat/completions",
13673                json!({
13674                    "model": "stub-model",
13675                    "messages": [{"role": "user", "content": "Return a JSON object."}],
13676                    "stream": true,
13677                    "stream_options": {"include_usage": true},
13678                    "response_format": {"type": "json_object"}
13679                }),
13680            )
13681            .await;
13682            let status = response.status();
13683            let body = response_text(response).await;
13684            assert_eq!(status, AxumStatusCode::OK, "{body}");
13685            let normalized = body.replace("\r\n", "\n");
13686            assert_eq!(normalized.matches("data: [DONE]").count(), 1, "{body}");
13687            assert!(normalized.ends_with("data: [DONE]\n\n"), "{body}");
13688            let events = responses_sse_json_events(&body);
13689            assert!(
13690                events.iter().all(|event| event.get("error").is_none()),
13691                "{body}"
13692            );
13693            let content: String = events
13694                .iter()
13695                .filter_map(|event| event["choices"][0]["delta"]["content"].as_str())
13696                .collect();
13697            let reasoning: String = events
13698                .iter()
13699                .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
13700                .collect();
13701            assert_eq!(content, r#"{"text":"<think>literal</think>"}"#);
13702            assert_eq!(reasoning, "reason");
13703            assert_eq!(
13704                serde_json::from_str::<Value>(&content).expect("intact JSON body"),
13705                json!({"text": "<think>literal</think>"})
13706            );
13707            let terminals: Vec<_> = events
13708                .iter()
13709                .enumerate()
13710                .filter(|(_, event)| !event["choices"][0]["finish_reason"].is_null())
13711                .collect();
13712            assert_eq!(terminals.len(), 1, "{body}");
13713            let (terminal_index, terminal) = terminals[0];
13714            assert_eq!(terminal["choices"][0]["finish_reason"], "stop");
13715            for event in &events[terminal_index..] {
13716                for field in ["content", "reasoning", "reasoning_content"] {
13717                    assert!(
13718                        event["choices"][0]["delta"][field]
13719                            .as_str()
13720                            .unwrap_or_default()
13721                            .is_empty(),
13722                        "payload after terminal: {event}"
13723                    );
13724                }
13725            }
13726            let usages: Vec<_> = events
13727                .iter()
13728                .enumerate()
13729                .filter(|(_, event)| !event["usage"].is_null())
13730                .collect();
13731            assert_eq!(usages.len(), 1, "{body}");
13732            let (usage_index, usage) = usages[0];
13733            assert!(terminal_index < usage_index, "{body}");
13734            assert_eq!(usage_index, events.len() - 1, "usage must be last: {body}");
13735            assert_eq!(usage["choices"], json!([]));
13736        }
13737    }
13738
13739    #[tokio::test]
13740    async fn json_object_rejects_non_json_model_output() {
13741        let request = chat_request(json!({
13742            "response_format": {"type": "json_object"}
13743        }));
13744        let err = chat_completions_handler(
13745            State(state_with_stub("not json")),
13746            HeaderMap::new(),
13747            Ok(Json(request)),
13748        )
13749        .await
13750        .expect_err("invalid json_object must fail");
13751        let (status, body) = error_json(err).await;
13752        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
13753        assert_eq!(body["error"]["type"], "internal_server_error");
13754        assert!(body["error"]["message"]
13755            .as_str()
13756            .unwrap_or_default()
13757            .contains("response_format.json_object"));
13758    }
13759
13760    #[test]
13761    fn one_of_strict_json_schema_reaches_hard_decoder() {
13762        let request = chat_request(json!({
13763            "response_format": {
13764                "type": "json_schema",
13765                "json_schema": {
13766                    "name": "unsupported",
13767                    "strict": true,
13768                    "schema": {"oneOf": [{"type": "string"}, {"type": "integer"}]}
13769                }
13770            }
13771        }));
13772        validate_chat_request(&request).expect("oneOf strict schema should validate");
13773        let internal = convert_chat_request(&request).expect("convert oneOf strict schema");
13774        let ferrum_types::ResponseFormat::JsonSchema(schema) =
13775            internal.sampling_params.response_format
13776        else {
13777            panic!("strict schema did not reach hard decoder");
13778        };
13779        assert_eq!(
13780            serde_json::from_str::<serde_json::Value>(&schema).unwrap()["oneOf"],
13781            json!([{"type": "string"}, {"type": "integer"}])
13782        );
13783        let schema = serde_json::from_str::<serde_json::Value>(&schema).unwrap();
13784        validate_json_text_against_schema(&schema, r#""answer""#)
13785            .expect("oneOf string branch should pass final validation");
13786        validate_json_text_against_schema(&schema, "7")
13787            .expect("oneOf integer branch should pass final validation");
13788        assert!(validate_json_text_against_schema(&schema, "true").is_err());
13789    }
13790
13791    #[tokio::test]
13792    async fn missing_json_schema_schema_rejects_with_field_param() {
13793        let request = chat_request(json!({
13794            "response_format": {
13795                "type": "json_schema",
13796                "json_schema": {
13797                    "name": "missing_schema",
13798                    "strict": true
13799                }
13800            }
13801        }));
13802        let err = chat_completions_handler(
13803            State(state_with_stub("unused")),
13804            HeaderMap::new(),
13805            Ok(Json(request)),
13806        )
13807        .await
13808        .expect_err("missing strict schema should reject");
13809        let (status, body) = error_json(err).await;
13810        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
13811        assert_eq!(body["error"]["param"], "response_format.json_schema");
13812        assert_eq!(body["error"]["type"], "invalid_request_error");
13813        assert!(body["error"]["message"]
13814            .as_str()
13815            .unwrap()
13816            .contains("schema is required"));
13817    }
13818
13819    #[test]
13820    fn non_strict_json_schema_is_preserved_but_not_hard_masked() {
13821        let request = chat_request(json!({
13822            "response_format": {
13823                "type": "json_schema",
13824                "json_schema": {
13825                    "name": "best_effort",
13826                    "strict": false,
13827                    "schema": {"oneOf": [{"type": "string"}, {"type": "integer"}]}
13828                }
13829            }
13830        }));
13831
13832        validate_chat_request(&request).expect("non-strict schema should not boundary reject");
13833        let internal = convert_chat_request(&request).expect("convert non-strict schema");
13834        assert!(
13835            internal
13836                .prompt
13837                .contains("response_format requires a single valid JSON value"),
13838            "response_format instruction should reach the model prompt: {}",
13839            internal.prompt
13840        );
13841        assert!(
13842            internal.prompt.contains("\"oneOf\""),
13843            "schema should reach the model prompt: {}",
13844            internal.prompt
13845        );
13846        assert_eq!(
13847            internal.sampling_params.response_format,
13848            ferrum_types::ResponseFormat::Text,
13849            "non-strict json_schema must stay best-effort instead of enabling hard guided decode"
13850        );
13851        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13852            panic!("expected structured chat api_request");
13853        };
13854        assert_eq!(
13855            api.response_format
13856                .as_ref()
13857                .and_then(|format| format.json_schema.as_ref())
13858                .and_then(|schema| schema.strict),
13859            Some(false)
13860        );
13861    }
13862
13863    #[test]
13864    fn json_object_response_format_instruction_reaches_model_prompt() {
13865        let request = chat_request(json!({
13866            "response_format": {"type": "json_object"}
13867        }));
13868
13869        let internal = convert_chat_request(&request).expect("convert json_object");
13870        assert!(
13871            internal
13872                .prompt
13873                .contains("response_format requires a single valid JSON object"),
13874            "response_format instruction should reach the model prompt: {}",
13875            internal.prompt
13876        );
13877        assert!(
13878            internal.prompt.contains("Output only JSON"),
13879            "JSON-only instruction should reach the model prompt: {}",
13880            internal.prompt
13881        );
13882        assert_eq!(
13883            internal.sampling_params.response_format,
13884            ferrum_types::ResponseFormat::JsonObject,
13885            "json_object must reach the tokenizer-aware hard decoder"
13886        );
13887        assert_eq!(
13888            internal.sampling_params.structured_output_start,
13889            StructuredOutputStart::Immediate
13890        );
13891    }
13892
13893    fn harmony_json_template() -> ModelChatTemplate {
13894        let mut template = ModelChatTemplate::new(
13895            "{% for message in messages %}<|start|>{{ message.role }}<|message|>{{ message.content }}<|end|>{% endfor %}{% if add_generation_prompt %}<|start|>assistant{% endif %}",
13896            "harmony-structured-template",
13897        );
13898        template.output_protocol = ModelOutputProtocol::HarmonyGptOss;
13899        template
13900    }
13901
13902    #[test]
13903    fn harmony_structured_format_activates_at_final_payload() {
13904        let template = harmony_json_template();
13905        for (response_format, constrained) in [
13906            (json!({"type": "json_object"}), true),
13907            (
13908                json!({
13909                    "type": "json_schema",
13910                    "json_schema": {
13911                        "name": "answer",
13912                        "strict": true,
13913                        "schema": {
13914                            "type": "object",
13915                            "properties": {"answer": {"type": "integer"}},
13916                            "required": ["answer"],
13917                            "additionalProperties": false
13918                        }
13919                    }
13920                }),
13921                true,
13922            ),
13923            (
13924                json!({
13925                    "type": "json_schema",
13926                    "json_schema": {
13927                        "name": "best_effort",
13928                        "strict": false,
13929                        "schema": {"type": "object"}
13930                    }
13931                }),
13932                false,
13933            ),
13934            (json!({"type": "text"}), false),
13935        ] {
13936            let request = chat_request(json!({"response_format": response_format}));
13937            let internal =
13938                convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13939                    .unwrap();
13940            assert_eq!(
13941                internal.sampling_params.structured_output_start,
13942                if constrained {
13943                    StructuredOutputStart::HarmonyFinal
13944                } else {
13945                    StructuredOutputStart::Immediate
13946                }
13947            );
13948            assert_eq!(
13949                internal.sampling_params.response_completion_boundary,
13950                ResponseCompletionBoundary::Immediate,
13951                "Harmony framing must not be gated on a Text reasoning delimiter"
13952            );
13953            internal.sampling_params.validate().unwrap();
13954        }
13955    }
13956
13957    fn harmony_json_request(stream: bool) -> Value {
13958        json!({
13959            "model": "stub-model",
13960            "messages": [{"role": "user", "content": "Return an answer object."}],
13961            "stream": stream,
13962            "response_format": {
13963                "type": "json_schema",
13964                "json_schema": {
13965                    "name": "answer",
13966                    "strict": true,
13967                    "schema": {
13968                        "type": "object",
13969                        "properties": {"answer": {"type": "integer"}},
13970                        "required": ["answer"],
13971                        "additionalProperties": false
13972                    }
13973                }
13974            }
13975        })
13976    }
13977
13978    #[tokio::test]
13979    async fn harmony_strict_json_routes_validate_final_payload_in_sync_and_sse() {
13980        for (chunks, finish_reason, reasoning) in [
13981            (
13982                vec![
13983                    "<|channel|>fi",
13984                    "nal<|message|>{\"answer\":",
13985                    "42}<|return|>",
13986                ],
13987                FinishReason::EOS,
13988                "",
13989            ),
13990            (
13991                vec![
13992                    "<|channel|>analysis<|message|>Compute.",
13993                    "<|end|><|start|>assistant<|channel|>fi",
13994                    "nal<|message|>{\"answer\":42}<|return|>",
13995                ],
13996                FinishReason::EOS,
13997                "Compute.",
13998            ),
13999            (
14000                vec!["<|channel|>final<|message|>{\"answer\":", "42}"],
14001                FinishReason::Length,
14002                "",
14003            ),
14004            (
14005                vec!["<|channel|>final<|message|>{\"answer\":", "42}"],
14006                FinishReason::Stop,
14007                "",
14008            ),
14009        ] {
14010            for stream in [false, true] {
14011                let engine = StubLlm {
14012                    finish_reason,
14013                    ..StubLlm::with_stream_chunks(&chunks)
14014                };
14015                let router = AxumServer::from_llm(Arc::new(engine))
14016                    .with_prompt_template(Some(harmony_json_template()))
14017                    .build_router();
14018                let mut request = harmony_json_request(stream);
14019                if finish_reason == FinishReason::Stop {
14020                    // An explicit stop at the model's terminal removes that
14021                    // marker while preserving the already complete JSON value.
14022                    request["stop"] = json!(["<|return|>"]);
14023                }
14024                let response = post_json(router, "/v1/chat/completions", request).await;
14025                assert_eq!(response.status(), AxumStatusCode::OK);
14026                if stream {
14027                    let body = response_text(response).await;
14028                    assert!(body.contains("data: [DONE]"));
14029                    let events = responses_sse_json_events(&body);
14030                    assert!(events.iter().all(|event| event.get("error").is_none()));
14031                    let content: String = events
14032                        .iter()
14033                        .filter_map(|event| event["choices"][0]["delta"]["content"].as_str())
14034                        .collect();
14035                    let actual_reasoning: String = events
14036                        .iter()
14037                        .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
14038                        .collect();
14039                    assert_eq!(
14040                        serde_json::from_str::<Value>(&content).unwrap(),
14041                        json!({"answer": 42})
14042                    );
14043                    assert_eq!(actual_reasoning, reasoning);
14044                } else {
14045                    let body = response_json(response).await;
14046                    let message = &body["choices"][0]["message"];
14047                    assert_eq!(message["content"], "{\"answer\":42}");
14048                    assert_eq!(message["reasoning"].as_str().unwrap_or(""), reasoning);
14049                }
14050            }
14051        }
14052    }
14053
14054    #[tokio::test]
14055    async fn harmony_strict_json_routes_reject_bad_framing_and_payload_without_sse_leaks() {
14056        for output in [
14057            "{\"answer\":42}",
14058            "<|channel|>final<|message|>{\"answer\":42}",
14059            "<|channel|>final<|message|>{\"answer\":42}<|call|>",
14060            "<|channel|>analysis<|message|>Compute.<|end|>\
14061             <|start|>assistant<|channel|>final<|message|>{\"answer\":\"wrong\"}<|return|>",
14062        ] {
14063            for stream in [false, true] {
14064                let response = post_json(
14065                    router_with_stub_and_template(output, harmony_json_template()),
14066                    "/v1/chat/completions",
14067                    harmony_json_request(stream),
14068                )
14069                .await;
14070                if stream {
14071                    assert_eq!(response.status(), AxumStatusCode::OK);
14072                    let body = response_text(response).await;
14073                    assert!(body.contains("data: [DONE]"));
14074                    let events = responses_sse_json_events(&body);
14075                    assert!(events.iter().any(|event| event.get("error").is_some()));
14076                    for event in events {
14077                        for field in ["content", "reasoning"] {
14078                            assert!(event["choices"][0]["delta"][field]
14079                                .as_str()
14080                                .unwrap_or("")
14081                                .is_empty());
14082                        }
14083                    }
14084                } else {
14085                    assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
14086                    let body = response_json(response).await;
14087                    assert_eq!(body["error"]["type"], "internal_server_error");
14088                    assert!(body.get("choices").is_none());
14089                }
14090            }
14091        }
14092    }
14093
14094    #[test]
14095    fn json_object_thinking_template_activates_after_typed_end_delimiter() {
14096        let request = chat_request(json!({
14097            "response_format": {"type": "json_object"}
14098        }));
14099        let template = ModelChatTemplate::new(
14100            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
14101            "thinking-test-template",
14102        );
14103
14104        assert_eq!(
14105            template.reasoning_protocol,
14106            ModelReasoningProtocol::PromptOpened
14107        );
14108        let internal =
14109            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14110                .expect("convert thinking json_object");
14111
14112        assert!(internal.prompt.ends_with("<assistant><think>\n"));
14113        assert!(internal
14114            .prompt
14115            .contains("Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text"));
14116        assert!(
14117            !internal.prompt.contains(THINK_END_TAG),
14118            "the instruction must not echo the typed end delimiter: {}",
14119            internal.prompt
14120        );
14121        assert_eq!(
14122            internal.sampling_params.structured_output_start,
14123            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
14124        );
14125        assert_eq!(
14126            internal.sampling_params.response_completion_boundary,
14127            ResponseCompletionBoundary::AfterDelimiterAndPayload {
14128                delimiter: THINK_END_TAG.to_string(),
14129                alternate_envelope: None,
14130            }
14131        );
14132    }
14133
14134    #[test]
14135    fn json_object_model_generated_thinking_activates_after_typed_end_delimiter() {
14136        let request = chat_request(json!({
14137            "response_format": {"type": "json_object"},
14138            "chat_template_kwargs": {"enable_thinking": true}
14139        }));
14140        let template = ModelChatTemplate::new(
14141            "{% 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 %}",
14142            "qwen3-model-generated-thinking-template",
14143        );
14144
14145        let internal =
14146            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14147                .expect("convert model-generated thinking json_object");
14148
14149        assert!(!has_unclosed_thinking_block(&internal.prompt));
14150        assert!(internal.prompt.ends_with("<assistant>"));
14151        assert!(internal
14152            .prompt
14153            .contains("Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text"));
14154        assert!(
14155            !internal.prompt.contains(THINK_START_TAG) && !internal.prompt.contains(THINK_END_TAG),
14156            "the instruction must not teach the model the typed reasoning delimiter: {}",
14157            internal.prompt
14158        );
14159        assert_eq!(
14160            internal.sampling_params.structured_output_start,
14161            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
14162        );
14163        assert_eq!(
14164            internal.sampling_params.response_completion_boundary,
14165            ResponseCompletionBoundary::AfterDelimiterAndPayload {
14166                delimiter: THINK_END_TAG.to_string(),
14167                alternate_envelope: None,
14168            }
14169        );
14170    }
14171
14172    #[test]
14173    fn strict_schema_model_generated_thinking_does_not_echo_typed_delimiter() {
14174        let request = chat_request(json!({
14175            "response_format": {
14176                "type": "json_schema",
14177                "json_schema": {
14178                    "name": "reasoning_result",
14179                    "strict": true,
14180                    "schema": {
14181                        "type": "object",
14182                        "properties": {
14183                            "result": {"type": "string", "const": "G00-c21-schema-OK"}
14184                        },
14185                        "required": ["result"],
14186                        "additionalProperties": false
14187                    }
14188                }
14189            },
14190            "chat_template_kwargs": {"enable_thinking": true}
14191        }));
14192        let template = ModelChatTemplate::new(
14193            "{% 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 %}",
14194            "qwen3-model-generated-thinking-template",
14195        );
14196
14197        let internal =
14198            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14199                .expect("convert model-generated thinking strict schema");
14200
14201        assert!(!has_unclosed_thinking_block(&internal.prompt));
14202        assert!(internal.prompt.ends_with("<assistant>"));
14203        assert!(internal.prompt.contains("G00-c21-schema-OK"));
14204        assert!(
14205            !internal.prompt.contains(THINK_START_TAG) && !internal.prompt.contains(THINK_END_TAG),
14206            "the instruction must not teach the model the typed reasoning delimiter: {}",
14207            internal.prompt
14208        );
14209        assert_eq!(
14210            internal.sampling_params.structured_output_start,
14211            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
14212        );
14213        assert_eq!(
14214            internal.sampling_params.response_completion_boundary,
14215            ResponseCompletionBoundary::AfterDelimiterAndPayload {
14216                delimiter: THINK_END_TAG.to_string(),
14217                alternate_envelope: None,
14218            }
14219        );
14220    }
14221
14222    #[test]
14223    fn json_object_model_generated_thinking_hard_off_starts_immediately() {
14224        let request = chat_request(json!({
14225            "response_format": {"type": "json_object"},
14226            "chat_template_kwargs": {"enable_thinking": false}
14227        }));
14228        let template = ModelChatTemplate::new(
14229            "{% 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 %}",
14230            "qwen3-model-generated-thinking-template",
14231        );
14232
14233        let internal =
14234            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14235                .expect("convert disabled model-generated thinking json_object");
14236
14237        assert_eq!(
14238            internal.sampling_params.structured_output_start,
14239            StructuredOutputStart::Immediate
14240        );
14241        assert_eq!(
14242            internal.sampling_params.response_completion_boundary,
14243            ResponseCompletionBoundary::Immediate
14244        );
14245        assert!(internal.prompt.contains("no chain-of-thought"));
14246    }
14247
14248    #[test]
14249    fn response_completion_contract_is_set_on_text_thinking_template() {
14250        let request = chat_request(json!({}));
14251        let template = ModelChatTemplate::new(
14252            "{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
14253            "thinking-test-template",
14254        );
14255
14256        let internal =
14257            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14258                .expect("convert thinking text request");
14259
14260        assert_eq!(
14261            internal.sampling_params.structured_output_start,
14262            StructuredOutputStart::Immediate
14263        );
14264        assert_eq!(
14265            internal.sampling_params.response_completion_boundary,
14266            ResponseCompletionBoundary::AfterDelimiterAndPayload {
14267                delimiter: THINK_END_TAG.to_string(),
14268                alternate_envelope: None,
14269            }
14270        );
14271    }
14272
14273    #[test]
14274    fn thinking_tool_request_compiles_typed_envelope_into_completion_contract() {
14275        let request = chat_request(json!({
14276            "tools": [{
14277                "type": "function",
14278                "function": {
14279                    "name": "weather",
14280                    "parameters": {
14281                        "type": "object",
14282                        "properties": {"city": {"type": "string"}},
14283                        "required": ["city"]
14284                    }
14285                }
14286            }]
14287        }));
14288        let template = ModelChatTemplate::new(
14289            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
14290            "thinking-tool-template",
14291        );
14292
14293        let internal =
14294            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
14295                .expect("convert thinking tool request");
14296
14297        assert_eq!(
14298            internal.sampling_params.response_completion_boundary,
14299            ResponseCompletionBoundary::AfterDelimiterAndPayload {
14300                delimiter: THINK_END_TAG.to_string(),
14301                alternate_envelope: Some(ferrum_types::ResponseCompletionEnvelope {
14302                    open_token_text: "<tool_call>".to_string(),
14303                    close_token_text: "</tool_call>".to_string(),
14304                    max_envelopes: 32,
14305                }),
14306            }
14307        );
14308    }
14309
14310    #[test]
14311    fn strict_json_schema_response_format_uses_guided_sampling_mode() {
14312        let request = chat_request(json!({
14313            "response_format": {
14314                "type": "json_schema",
14315                "json_schema": {
14316                    "name": "answer",
14317                    "strict": true,
14318                    "schema": {
14319                        "type": "object",
14320                        "properties": {"answer": {"type": "string"}},
14321                        "required": ["answer"]
14322                    }
14323                }
14324            }
14325        }));
14326
14327        let internal = convert_chat_request(&request).expect("convert strict json_schema");
14328        assert!(
14329            internal
14330                .prompt
14331                .contains("response_format requires a single valid JSON value"),
14332            "response_format instruction should reach the model prompt: {}",
14333            internal.prompt
14334        );
14335        let ferrum_types::ResponseFormat::JsonSchema(schema) =
14336            internal.sampling_params.response_format
14337        else {
14338            panic!(
14339                "strict json_schema must reach guided decoding, got {:?}",
14340                internal.sampling_params.response_format
14341            );
14342        };
14343        let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
14344        assert_eq!(schema["type"], "object");
14345        assert_eq!(schema["properties"]["answer"]["type"], "string");
14346        assert_eq!(schema["required"], json!(["answer"]));
14347    }
14348
14349    #[tokio::test]
14350    async fn strict_json_schema_validates_non_streaming_response() {
14351        let request = chat_request(json!({
14352            "response_format": {
14353                "type": "json_schema",
14354                "json_schema": {
14355                    "name": "answer",
14356                    "strict": true,
14357                    "schema": {
14358                        "type": "object",
14359                        "properties": {"answer": {"type": "string"}},
14360                        "required": ["answer"]
14361                    }
14362                }
14363            }
14364        }));
14365        let response = chat_completions_handler(
14366            State(state_with_stub("{\"answer\":\"yes\"}")),
14367            HeaderMap::new(),
14368            Ok(Json(request)),
14369        )
14370        .await
14371        .expect("strict response");
14372        assert_eq!(response.status(), AxumStatusCode::OK);
14373        let body = response_json(response).await;
14374        assert_eq!(
14375            body["choices"][0]["message"]["content"],
14376            "{\"answer\":\"yes\"}"
14377        );
14378    }
14379
14380    #[tokio::test]
14381    async fn strict_json_schema_validates_non_streaming_response_after_reasoning_block() {
14382        let request = chat_request(json!({
14383            "response_format": {
14384                "type": "json_schema",
14385                "json_schema": {
14386                    "name": "answer",
14387                    "strict": true,
14388                    "schema": {
14389                        "type": "object",
14390                        "properties": {"answer": {"type": "string"}},
14391                        "required": ["answer"]
14392                    }
14393                }
14394            }
14395        }));
14396        let response = chat_completions_handler(
14397            State(state_with_stub(
14398                "<think>\nreasoning\n</think>\n\n{\"answer\":\"yes\"}",
14399            )),
14400            HeaderMap::new(),
14401            Ok(Json(request)),
14402        )
14403        .await
14404        .expect("strict response with reasoning");
14405        assert_eq!(response.status(), AxumStatusCode::OK);
14406        let body = response_json(response).await;
14407        assert_eq!(
14408            body["choices"][0]["message"]["content"],
14409            "{\"answer\":\"yes\"}"
14410        );
14411        assert_eq!(body["choices"][0]["message"]["reasoning"], "\nreasoning\n");
14412    }
14413
14414    #[tokio::test]
14415    async fn strict_json_schema_validates_streaming_final_response() {
14416        let response = post_json(
14417            router_with_stub("{\"answer\":\"yes\"}"),
14418            "/v1/chat/completions",
14419            json!({
14420                "model": "stub-model",
14421                "messages": [{"role": "user", "content": "Return an answer object."}],
14422                "stream": true,
14423                "response_format": {
14424                    "type": "json_schema",
14425                    "json_schema": {
14426                        "name": "answer",
14427                        "strict": true,
14428                        "schema": {
14429                            "type": "object",
14430                            "properties": {"answer": {"type": "string"}},
14431                            "required": ["answer"]
14432                        }
14433                    }
14434                }
14435            }),
14436        )
14437        .await;
14438        assert_eq!(response.status(), AxumStatusCode::OK);
14439        let body = response_text(response).await;
14440        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
14441        assert!(
14442            body.contains("\\\"answer\\\":\\\"yes\\\""),
14443            "strict streaming content missing: {body}"
14444        );
14445        assert!(
14446            !body.contains("\"error\""),
14447            "valid strict streaming response should not emit error: {body}"
14448        );
14449    }
14450
14451    #[tokio::test]
14452    async fn strict_json_schema_validates_streaming_final_response_after_reasoning_block() {
14453        let response = post_json(
14454            router_with_stub_stream_chunks(&[
14455                "<think>\nreasoning",
14456                "\n</think>\n\n",
14457                "{\"answer\":\"yes\"}",
14458            ]),
14459            "/v1/chat/completions",
14460            json!({
14461                "model": "stub-model",
14462                "messages": [{"role": "user", "content": "Return an answer object."}],
14463                "stream": true,
14464                "response_format": {
14465                    "type": "json_schema",
14466                    "json_schema": {
14467                        "name": "answer",
14468                        "strict": true,
14469                        "schema": {
14470                            "type": "object",
14471                            "properties": {"answer": {"type": "string"}},
14472                            "required": ["answer"]
14473                        }
14474                    }
14475                }
14476            }),
14477        )
14478        .await;
14479        assert_eq!(response.status(), AxumStatusCode::OK);
14480        let body = response_text(response).await;
14481        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
14482        assert!(
14483            body.contains("\\\"answer\\\":\\\"yes\\\""),
14484            "strict streaming content missing: {body}"
14485        );
14486        assert!(
14487            body.contains(r#""reasoning":"\nreasoning\n""#),
14488            "strict streaming should keep reasoning separate: {body}"
14489        );
14490        assert!(
14491            !body.contains("\"error\""),
14492            "valid strict streaming response should not emit error: {body}"
14493        );
14494    }
14495
14496    #[tokio::test]
14497    async fn strict_json_schema_invalid_streaming_output_emits_error_event() {
14498        let response = post_json(
14499            router_with_stub("not json"),
14500            "/v1/chat/completions",
14501            json!({
14502                "model": "stub-model",
14503                "messages": [{"role": "user", "content": "Return an answer object."}],
14504                "stream": true,
14505                "response_format": {
14506                    "type": "json_schema",
14507                    "json_schema": {
14508                        "name": "answer",
14509                        "strict": true,
14510                        "schema": {
14511                            "type": "object",
14512                            "properties": {"answer": {"type": "string"}},
14513                            "required": ["answer"]
14514                        }
14515                    }
14516                }
14517            }),
14518        )
14519        .await;
14520        assert_eq!(response.status(), AxumStatusCode::OK);
14521        let body = response_text(response).await;
14522        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
14523        assert!(
14524            body.contains("\"type\":\"internal_server_error\""),
14525            "strict streaming validation failure should emit OpenAI error: {body}"
14526        );
14527        assert!(
14528            body.contains("\"param\":\"response_format.json_schema\""),
14529            "strict streaming validation error should identify schema param: {body}"
14530        );
14531        assert!(
14532            body.contains("invalid JSON"),
14533            "strict streaming validation should report invalid JSON: {body}"
14534        );
14535        assert!(
14536            !body.contains("not json"),
14537            "strict streaming must not emit invalid partial deltas before validation failure: {body}"
14538        );
14539    }
14540
14541    #[tokio::test]
14542    async fn route_strict_json_schema_supported_schema_passes_100_runs() {
14543        let request_body = json!({
14544            "model": "stub-model",
14545            "messages": [{"role": "user", "content": "Return an answer object."}],
14546            "response_format": {
14547                "type": "json_schema",
14548                "json_schema": {
14549                    "name": "answer",
14550                    "strict": true,
14551                    "schema": {
14552                        "type": "object",
14553                        "properties": {"answer": {"type": "string"}},
14554                        "required": ["answer"]
14555                    }
14556                }
14557            }
14558        });
14559        let router = router_with_stub("{\"answer\":\"yes\"}");
14560        for run in 0..100 {
14561            let response =
14562                post_json(router.clone(), "/v1/chat/completions", request_body.clone()).await;
14563            assert_eq!(
14564                response.status(),
14565                AxumStatusCode::OK,
14566                "strict schema run {run} returned non-200"
14567            );
14568            let body = response_json(response).await;
14569            let content = body["choices"][0]["message"]["content"]
14570                .as_str()
14571                .unwrap_or("");
14572            assert_eq!(
14573                content, "{\"answer\":\"yes\"}",
14574                "strict schema run {run} returned unexpected content"
14575            );
14576            let parsed: serde_json::Value =
14577                serde_json::from_str(content).expect("strict content JSON");
14578            assert_eq!(parsed["answer"], "yes");
14579        }
14580    }
14581
14582    #[test]
14583    fn server_prefix_prompt_observation_follows_engine_authority() {
14584        let policy = CachePolicy {
14585            prefix_cache_enabled: true,
14586            session_cache_mode: "off".to_string(),
14587            session_cache_max_entries: 128,
14588            session_cache_max_tokens: 4096,
14589        };
14590        for authority in [
14591            ExecutionResourceAuthority::PlanRuntime,
14592            ExecutionResourceAuthority::LegacyEngine,
14593        ] {
14594            let engine = StubLlm {
14595                resource_authority: authority,
14596                ..StubLlm::new("ok")
14597            };
14598            // There is deliberately no cache metrics snapshot. Native authority
14599            // alone must skip the text fallback, including unsupported plans.
14600            assert!(engine.cache_metrics_snapshot().is_none());
14601            let state = AppState::default().with_llm(Arc::new(engine));
14602            state.record_prefix_prompt("alpha beta gamma", &policy);
14603            state.record_prefix_prompt("alpha beta delta", &policy);
14604            let stats = state.cache.stats();
14605            let prompts = state.cache.prefix_prompts.lock().unwrap();
14606            match authority {
14607                ExecutionResourceAuthority::PlanRuntime => {
14608                    assert!(prompts.is_empty());
14609                    assert_eq!(stats.prefix_entries, 0);
14610                    assert_eq!(stats.prefix_bytes, 0);
14611                    assert_eq!(stats.prefix_hits, 0);
14612                    assert_eq!(stats.prefix_misses, 0);
14613                    assert_eq!(stats.prefix_saved_prefill_tokens, 0);
14614                }
14615                ExecutionResourceAuthority::LegacyEngine => {
14616                    assert_eq!(prompts.len(), 2);
14617                    assert!(prompts.contains_key("alpha beta gamma"));
14618                    assert!(prompts.contains_key("alpha beta delta"));
14619                    assert_eq!(stats.prefix_entries, 2);
14620                    assert_eq!(stats.prefix_bytes, 32);
14621                    assert_eq!(stats.prefix_hits, 1);
14622                    assert_eq!(stats.prefix_misses, 1);
14623                    assert!(stats.prefix_saved_prefill_tokens > 0);
14624                }
14625            }
14626        }
14627    }
14628
14629    #[test]
14630    fn cache_metrics_use_engine_real_kv_snapshot_when_available() {
14631        let cache = CacheRuntimeState::default();
14632        let policy = CachePolicy {
14633            prefix_cache_enabled: true,
14634            session_cache_mode: "memory".to_string(),
14635            session_cache_max_entries: 128,
14636            session_cache_max_tokens: 4096,
14637        };
14638        cache.record_prefix_prompt("alpha beta gamma", &policy);
14639        cache.record_prefix_prompt("alpha beta delta", &policy);
14640
14641        let engine_snapshot = json!({
14642            "position": "real-kv-reuse",
14643            "source": "llama-family-paged-block-prefix-cache",
14644            "enabled": true,
14645            "hits": 7,
14646            "misses": 3,
14647            "evictions": 1,
14648            "saved_prefill_tokens": 64,
14649            "entries": 5,
14650            "bytes": 8192,
14651            "block_size": 16,
14652            "kv_dtype": "fp16",
14653            "selected_pipeline_mode": "batch",
14654            "selected_stage_bridge": "host",
14655            "stage_count": 2,
14656        });
14657
14658        let health = cache.health_json(&policy, Some(&engine_snapshot));
14659        let prefix = &health["prefix_cache"];
14660        assert_eq!(prefix["position"], "real-kv-reuse");
14661        assert_eq!(prefix["source"], "llama-family-paged-block-prefix-cache");
14662        assert_eq!(prefix["hits"], 7);
14663        assert_eq!(prefix["misses"], 3);
14664        assert_eq!(prefix["evictions"], 1);
14665        assert_eq!(prefix["saved_prefill_tokens"], 64);
14666        assert_eq!(prefix["entries"], 5);
14667        assert_eq!(prefix["bytes"], 8192);
14668        assert_eq!(prefix["block_size"], 16);
14669        assert_eq!(prefix["kv_dtype"], "fp16");
14670        assert_eq!(prefix["selected_pipeline_mode"], "batch");
14671        assert_eq!(prefix["selected_stage_bridge"], "host");
14672        assert_eq!(prefix["stage_count"], 2);
14673
14674        let metrics = cache.prometheus_metrics(Some(&engine_snapshot));
14675        assert!(metrics.contains("ferrum_prefix_cache_hits_total 7\n"));
14676        assert!(metrics.contains("ferrum_prefix_cache_misses_total 3\n"));
14677        assert!(metrics.contains("ferrum_prefix_cache_saved_prefill_tokens_total 64\n"));
14678        assert!(metrics.contains("ferrum_prefix_cache_entries 5\n"));
14679        assert!(metrics.contains("ferrum_prefix_cache_bytes 8192\n"));
14680    }
14681
14682    #[tokio::test]
14683    async fn strict_json_schema_invalid_model_output_fails_before_response() {
14684        let request = chat_request(json!({
14685            "response_format": {
14686                "type": "json_schema",
14687                "json_schema": {
14688                    "name": "answer",
14689                    "strict": true,
14690                    "schema": {
14691                        "type": "object",
14692                        "properties": {"answer": {"type": "string"}},
14693                        "required": ["answer"]
14694                    }
14695                }
14696            }
14697        }));
14698        let err = chat_completions_handler(
14699            State(state_with_stub("not json")),
14700            HeaderMap::new(),
14701            Ok(Json(request)),
14702        )
14703        .await
14704        .expect_err("invalid strict response should fail");
14705        let (status, body) = error_json(err).await;
14706        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
14707        assert_eq!(body["error"]["type"], "internal_server_error");
14708        assert!(body["error"]["message"]
14709            .as_str()
14710            .unwrap()
14711            .contains("json_schema.strict"));
14712    }
14713
14714    #[tokio::test]
14715    async fn strict_json_schema_does_not_rely_on_markdown_fence_stripping() {
14716        let request = chat_request(json!({
14717            "response_format": {
14718                "type": "json_schema",
14719                "json_schema": {
14720                    "name": "answer",
14721                    "strict": true,
14722                    "schema": {
14723                        "type": "object",
14724                        "properties": {"answer": {"type": "string"}},
14725                        "required": ["answer"]
14726                    }
14727                }
14728            }
14729        }));
14730        let err = chat_completions_handler(
14731            State(state_with_stub("```json\n{\"answer\":\"yes\"}\n```")),
14732            HeaderMap::new(),
14733            Ok(Json(request)),
14734        )
14735        .await
14736        .expect_err("strict schema should fail fenced JSON instead of repairing it");
14737        let (status, body) = error_json(err).await;
14738        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
14739        assert_eq!(body["error"]["type"], "internal_server_error");
14740        assert!(body["error"]["message"]
14741            .as_str()
14742            .unwrap()
14743            .contains("json_schema.strict: invalid JSON"));
14744    }
14745}