1use crate::config::CliConfig;
4use chrono::Utc;
5use clap::{Args, ValueEnum};
6use colored::*;
7use console::{measure_text_width, Key, Term};
8use ferrum_models::source::{ModelFormat, ResolvedModelSource};
9use ferrum_server::chat_template::{ChatTemplateOptions, ModelChatTemplate, PromptMessage};
10use ferrum_types::{
11 has_unclosed_thinking_block, parse_reasoning_response_for_prompt, FerrumConfigBuilder,
12 FerrumError, FinishReason, InferenceRequest, InferenceResponse, ModelCapabilities, Priority,
13 RequestId, ResolvedFerrumConfig, ResponseCompletionBoundary, Result, RuntimeConfigEntry,
14 RuntimeConfigSnapshot, RuntimeConfigSource, SamplingParams, StreamChunk, TokenUsage,
15 WorkloadProfile, DEFAULT_CHAT_REPETITION_PENALTY, THINK_END_TAG, THINK_START_TAG,
16};
17use futures::StreamExt;
18use indicatif::{ProgressBar, ProgressStyle};
19use sha2::{Digest, Sha256};
20use std::collections::HashMap;
21use std::io::{self, BufRead, IsTerminal, Write};
22#[cfg(unix)]
23use std::mem;
24#[cfg(unix)]
25use std::os::fd::{AsRawFd, RawFd};
26use std::path::{Path, PathBuf};
27use std::pin::Pin;
28use uuid::Uuid;
29
30#[cfg(test)]
31use crate::source_resolver::tokenizer_sibling_repo;
32
33const RUN_INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY: &str = "ferrum_initial_forbidden_token_texts";
34const RUN_JSONL_SCHEMA_VERSION: u32 = 2;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum, Default)]
40pub enum OutputFormat {
41 #[default]
43 Text,
44 Jsonl,
46}
47
48fn finish_reason_str(r: FinishReason) -> &'static str {
49 match r {
50 FinishReason::Length => "length",
51 FinishReason::Stop => "stop",
52 FinishReason::EOS => "eos",
53 FinishReason::Cancelled => "cancelled",
54 FinishReason::Error => "error",
55 FinishReason::ContentFilter => "content_filter",
56 }
57}
58
59fn emit_jsonl_ready(session_id: &str, requested_model: &str, resolved_model: &str, backend: &str) {
60 let record = serde_json::json!({
61 "schema_version": RUN_JSONL_SCHEMA_VERSION,
62 "event": "ready",
63 "session_id": session_id,
64 "history_epoch": 0,
65 "model": resolved_model,
66 "requested_model": requested_model,
67 "resolved_model": resolved_model,
68 "backend": backend,
69 });
70 emit_jsonl_record(&record);
71}
72
73fn emit_jsonl_user(
74 session_id: &str,
75 history_epoch: usize,
76 request_id: &str,
77 turn: usize,
78 content: &str,
79 history: &[(String, String)],
80) {
81 let record = serde_json::json!({
82 "schema_version": RUN_JSONL_SCHEMA_VERSION,
83 "event": "user",
84 "session_id": session_id,
85 "history_epoch": history_epoch,
86 "request_id": request_id,
87 "turn": turn,
88 "content": content,
89 "history_before": history_evidence(history),
90 });
91 emit_jsonl_record(&record);
92}
93
94fn emit_jsonl_assistant_delta(
95 session_id: &str,
96 history_epoch: usize,
97 request_id: &str,
98 turn: usize,
99 index: usize,
100 raw_text_delta: &str,
101 token_id: Option<u32>,
102) {
103 emit_jsonl_record(&jsonl_assistant_delta_record(
104 session_id,
105 history_epoch,
106 request_id,
107 turn,
108 index,
109 raw_text_delta,
110 token_id,
111 ));
112}
113
114fn jsonl_assistant_delta_record(
115 session_id: &str,
116 history_epoch: usize,
117 request_id: &str,
118 turn: usize,
119 index: usize,
120 raw_text_delta: &str,
121 token_id: Option<u32>,
122) -> serde_json::Value {
123 serde_json::json!({
124 "schema_version": RUN_JSONL_SCHEMA_VERSION,
125 "event": "assistant_delta",
126 "session_id": session_id,
127 "history_epoch": history_epoch,
128 "request_id": request_id,
129 "turn": turn,
130 "index": index,
131 "raw_text_delta": raw_text_delta,
132 "utf8_bytes": raw_text_delta.len(),
133 "token_id": token_id,
134 })
135}
136
137fn emit_jsonl_assistant(
138 session_id: &str,
139 history_epoch: usize,
140 request_id: &str,
141 turn: usize,
142 content: &str,
143 reasoning: Option<&str>,
144 history: &[(String, String)],
145 finish_reason: Option<FinishReason>,
146 usage: Option<&TokenUsage>,
147 n_tokens: usize,
148 chunk_count: usize,
149 raw_text: &str,
150 ms: f64,
151) {
152 emit_jsonl_record(&jsonl_assistant_record(
153 session_id,
154 history_epoch,
155 request_id,
156 turn,
157 content,
158 reasoning,
159 history,
160 finish_reason,
161 usage,
162 n_tokens,
163 chunk_count,
164 raw_text,
165 ms,
166 ));
167}
168
169fn jsonl_assistant_record(
170 session_id: &str,
171 history_epoch: usize,
172 request_id: &str,
173 turn: usize,
174 content: &str,
175 reasoning: Option<&str>,
176 history: &[(String, String)],
177 finish_reason: Option<FinishReason>,
178 usage: Option<&TokenUsage>,
179 n_tokens: usize,
180 chunk_count: usize,
181 raw_text: &str,
182 ms: f64,
183) -> serde_json::Value {
184 serde_json::json!({
185 "schema_version": RUN_JSONL_SCHEMA_VERSION,
186 "event": "assistant",
187 "session_id": session_id,
188 "history_epoch": history_epoch,
189 "request_id": request_id,
190 "turn": turn,
191 "content": content,
192 "reasoning": reasoning,
193 "history_before": history_evidence(history),
194 "finish_reason": finish_reason.map(finish_reason_str),
195 "usage": usage,
196 "n_tokens": n_tokens,
197 "chunk_count": chunk_count,
198 "raw_text_sha256": sha256_text(raw_text),
199 "ms": ms,
200 })
201}
202
203fn emit_jsonl_exit(session_id: &str, history_epoch: usize, reason: &str) {
204 let record = serde_json::json!({
205 "schema_version": RUN_JSONL_SCHEMA_VERSION,
206 "event": "exit",
207 "session_id": session_id,
208 "history_epoch": history_epoch,
209 "reason": reason,
210 });
211 emit_jsonl_record(&record);
212}
213
214fn emit_jsonl_record(record: &serde_json::Value) {
215 println!("{record}");
216 io::stdout().flush().ok();
217}
218
219fn sha256_text(value: &str) -> String {
220 format!("{:x}", Sha256::digest(value.as_bytes()))
221}
222
223fn history_evidence(history: &[(String, String)]) -> serde_json::Value {
224 let encoded = serde_json::to_vec(history).expect("run history serialization cannot fail");
225 serde_json::json!({
226 "message_count": history.len(),
227 "turn_count": history.iter().filter(|(role, _)| role == "user").count(),
228 "sha256": format!("{:x}", Sha256::digest(encoded)),
229 })
230}
231
232fn run_request_metadata(
233 prompt: &str,
234 chat_template_options: &ChatTemplateOptions,
235) -> HashMap<String, serde_json::Value> {
236 let mut metadata = HashMap::new();
237 if !has_unclosed_thinking_block(prompt) {
238 let mut forbidden = vec![serde_json::Value::String(THINK_END_TAG.to_string())];
239 if chat_template_options.enable_thinking == Some(false) {
240 forbidden.push(serde_json::Value::String(THINK_START_TAG.to_string()));
241 }
242 metadata.insert(
243 RUN_INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY.to_string(),
244 serde_json::Value::Array(forbidden),
245 );
246 }
247 metadata
248}
249
250#[derive(Debug, Clone)]
251struct RunPromptPlan {
252 prompt: String,
253 sampling_params: SamplingParams,
254 prompt_token_ids: Option<Vec<u32>>,
255 prompt_tokens: Option<usize>,
256 kv_capacity: Option<usize>,
257 dropped_history_messages: usize,
258 dropped_history_turns: usize,
259 max_tokens_clamped_from: Option<usize>,
260}
261
262#[derive(Debug, Clone)]
263struct RunPromptTokenization {
264 token_ids: Option<Vec<u32>>,
265 token_count: Option<usize>,
266}
267
268struct RunBudget {
269 tokenizer: Option<tokenizers::Tokenizer>,
270 kv_capacity: Option<usize>,
271 #[cfg(test)]
272 prompt_token_id_mapper: Option<fn(&str) -> Vec<u32>>,
273 #[cfg(test)]
274 prompt_token_counter: Option<fn(&str) -> usize>,
275}
276
277impl RunBudget {
278 fn from_product_sources(
279 explicit_tokenizer: Option<&Path>,
280 product_sources: Option<&ferrum_models::vnext::ProductionModelSourceBundle>,
281 legacy_source_path: &Path,
282 snapshot: &RuntimeConfigSnapshot,
283 ) -> Result<Self> {
284 let tokenizer = if let Some(path) = explicit_tokenizer {
285 Some(tokenizers::Tokenizer::from_file(path).map_err(|error| {
286 FerrumError::model(format!(
287 "failed to load explicit tokenizer {}: {error}",
288 path.display()
289 ))
290 })?)
291 } else if let Some(sources) = product_sources {
292 Some(
293 tokenizers::Tokenizer::from_bytes(sources.tokenizer_json()).map_err(|error| {
294 FerrumError::model(format!(
295 "failed to parse tokenizer from product source {}: {error}",
296 sources.tokenizer_file().display()
297 ))
298 })?,
299 )
300 } else {
301 discover_run_tokenizer_path(legacy_source_path)
302 .and_then(|path| tokenizers::Tokenizer::from_file(path).ok())
303 };
304 let kv_capacity =
305 crate::runtime_env::runtime_snapshot_value(snapshot, "FERRUM_KV_CAPACITY")
306 .and_then(|value| value.parse::<usize>().ok())
307 .filter(|&value| value > 0);
308 Ok(Self {
309 tokenizer,
310 kv_capacity,
311 #[cfg(test)]
312 prompt_token_id_mapper: None,
313 #[cfg(test)]
314 prompt_token_counter: None,
315 })
316 }
317
318 fn prompt_tokenization(&self, prompt: &str) -> RunPromptTokenization {
319 #[cfg(test)]
320 if let Some(mapper) = self.prompt_token_id_mapper {
321 let token_ids = mapper(prompt);
322 return RunPromptTokenization {
323 token_count: Some(token_ids.len()),
324 token_ids: Some(token_ids),
325 };
326 }
327 #[cfg(test)]
328 if let Some(counter) = self.prompt_token_counter {
329 return RunPromptTokenization {
330 token_ids: None,
331 token_count: Some(counter(prompt)),
332 };
333 }
334 if let Some(encoding) = self
335 .tokenizer
336 .as_ref()
337 .and_then(|tok| tok.encode(prompt, true).ok())
338 {
339 let token_ids = encoding.get_ids().to_vec();
340 return RunPromptTokenization {
341 token_count: Some(token_ids.len()),
342 token_ids: Some(token_ids),
343 };
344 }
345 RunPromptTokenization {
346 token_ids: None,
347 token_count: None,
348 }
349 }
350}
351
352fn display_response_text(text: &str) -> String {
353 text.trim().to_string()
354}
355
356struct CollectedRunGeneration {
357 request_id: String,
358 raw_text: String,
359 finish_reason: Option<FinishReason>,
360 usage: Option<TokenUsage>,
361 token_count: usize,
362 token_ids: Vec<u32>,
363 chunk_count: usize,
364 execution_evidence: Option<ferrum_types::InferenceExecutionEvidence>,
365}
366
367impl CollectedRunGeneration {
368 fn from_response(response: InferenceResponse) -> Self {
369 Self {
370 request_id: response.request_id.to_string(),
371 raw_text: response.text,
372 finish_reason: Some(response.finish_reason),
373 usage: Some(response.usage),
374 token_count: response.tokens.len(),
375 token_ids: response
376 .tokens
377 .into_iter()
378 .map(|token| token.get())
379 .collect(),
380 chunk_count: 1,
381 execution_evidence: response.execution_evidence,
382 }
383 }
384}
385
386type RunResponseStream = Pin<Box<dyn futures::Stream<Item = Result<StreamChunk>> + Send + 'static>>;
387
388async fn collect_run_stream(
389 mut stream: RunResponseStream,
390 trace_tokens: bool,
391 turn: usize,
392 session_id: &str,
393 history_epoch: usize,
394 expected_request_id: &str,
395) -> Result<CollectedRunGeneration> {
396 let mut request_id = None;
397 let mut raw_text = String::new();
398 let mut finish_reason = None;
399 let mut latest_usage = None;
400 let mut token_count = 0usize;
401 let mut token_ids = Vec::new();
402 let mut chunk_count = 0usize;
403 let mut execution_evidence = None;
404 while let Some(chunk) = stream.next().await {
405 let mut chunk = chunk?;
406 let chunk_request_id = chunk.request_id.to_string();
407 if chunk_request_id != expected_request_id {
408 return Err(FerrumError::internal(format!(
409 "run stream request id drift: expected {expected_request_id}, got {chunk_request_id}"
410 )));
411 }
412 request_id.get_or_insert_with(|| chunk_request_id.clone());
413 let token_id = chunk.token.map(|token| token.get());
414 if !chunk.text.is_empty() {
415 raw_text.push_str(&chunk.text);
416 emit_jsonl_assistant_delta(
417 session_id,
418 history_epoch,
419 expected_request_id,
420 turn,
421 chunk_count,
422 &chunk.text,
423 token_id,
424 );
425 chunk_count += 1;
426 }
427 if let Some(token_id) = token_id {
428 if trace_tokens {
429 eprintln!(
430 "[run-token-trace] turn={turn} token={} text={:?}",
431 token_id, chunk.text
432 );
433 }
434 token_ids.push(token_id);
435 token_count += 1;
436 }
437 if let Some(usage) = chunk.usage.as_ref() {
438 token_count = usage.completion_tokens;
439 latest_usage = Some(usage.clone());
440 }
441 if chunk.finish_reason.is_some() {
442 finish_reason = chunk.finish_reason;
443 }
444 if let Some(evidence) = chunk.execution_evidence.take() {
445 if execution_evidence.replace(evidence).is_some() {
446 return Err(FerrumError::internal(
447 "run stream emitted engine execution evidence more than once",
448 ));
449 }
450 }
451 }
452 Ok(CollectedRunGeneration {
453 request_id: request_id.unwrap_or_else(|| expected_request_id.to_string()),
454 raw_text,
455 finish_reason,
456 usage: latest_usage,
457 token_count,
458 token_ids,
459 chunk_count,
460 execution_evidence,
461 })
462}
463
464async fn collect_run_text_stream(
465 mut stream: RunResponseStream,
466 trace_tokens: bool,
467 turn: usize,
468 expected_request_id: &RequestId,
469 stdin_is_tty: bool,
470 capture_token_ids: bool,
471) -> Result<CollectedRunGeneration> {
472 let mut first_token_indicator = start_first_token_indicator(stdin_is_tty);
473 let mut request_id = None;
474 let mut raw_text = String::new();
475 let mut finish_reason = None;
476 let mut latest_usage = None;
477 let mut token_count = 0usize;
478 let mut token_ids = Vec::new();
479 let mut chunk_count = 0usize;
480 let mut execution_evidence = None;
481 while let Some(chunk) = stream.next().await {
482 let mut chunk = match chunk {
483 Ok(chunk) => chunk,
484 Err(error) => {
485 clear_first_token_indicator(&mut first_token_indicator);
486 return Err(error);
487 }
488 };
489 if &chunk.request_id != expected_request_id {
490 clear_first_token_indicator(&mut first_token_indicator);
491 return Err(FerrumError::internal(format!(
492 "run stream request id drift: expected {expected_request_id}, got {}",
493 chunk.request_id
494 )));
495 }
496 request_id.get_or_insert_with(|| chunk.request_id.clone());
497 let token_id = chunk.token.map(|token| token.get());
498 if first_token_indicator.is_some()
499 && (!chunk.text.is_empty() || token_id.is_some() || chunk.finish_reason.is_some())
500 {
501 clear_first_token_indicator(&mut first_token_indicator);
502 }
503 if trace_tokens {
504 if let Some(token_id) = token_id {
505 eprintln!(
506 "[run-token-trace] turn={turn} token={} text={:?}",
507 token_id, chunk.text
508 );
509 }
510 }
511 if !chunk.text.is_empty() {
512 raw_text.push_str(&chunk.text);
513 print!("{}", chunk.text);
514 io::stdout().flush().ok();
515 chunk_count += 1;
516 }
517 if let Some(token_id) = token_id {
518 if capture_token_ids {
519 token_ids.push(token_id);
520 }
521 token_count += 1;
522 }
523 if let Some(usage) = chunk.usage.as_ref() {
524 token_count = usage.completion_tokens;
525 latest_usage = Some(usage.clone());
526 }
527 if chunk.finish_reason.is_some() {
528 finish_reason = chunk.finish_reason;
529 }
530 if let Some(evidence) = chunk.execution_evidence.take() {
531 if execution_evidence.replace(evidence).is_some() {
532 clear_first_token_indicator(&mut first_token_indicator);
533 return Err(FerrumError::internal(
534 "run stream emitted engine execution evidence more than once",
535 ));
536 }
537 }
538 }
539 clear_first_token_indicator(&mut first_token_indicator);
540 Ok(CollectedRunGeneration {
541 request_id: request_id
542 .unwrap_or_else(|| expected_request_id.clone())
543 .to_string(),
544 raw_text,
545 finish_reason,
546 usage: latest_usage,
547 token_count,
548 token_ids,
549 chunk_count,
550 execution_evidence,
551 })
552}
553
554#[derive(Args)]
555pub struct RunCommand {
556 #[arg(value_name = "MODEL")]
558 pub model: Option<String>,
559
560 #[command(flatten)]
561 pub product_sources: crate::source_resolver::ProductSourceArgs,
562
563 #[arg(long)]
565 pub system: Option<String>,
566
567 #[arg(long, default_value = "4096")]
570 pub max_tokens: u32,
571
572 #[arg(long, value_name = "TEXT")]
574 pub stop: Vec<String>,
575
576 #[arg(long, conflicts_with = "disable_thinking")]
578 pub enable_thinking: bool,
579
580 #[arg(long, conflicts_with = "enable_thinking")]
582 pub disable_thinking: bool,
583
584 #[arg(long)]
589 pub no_context_shift: bool,
590
591 #[arg(long, default_value = "0.0")]
595 pub temperature: f32,
596
597 #[arg(long, default_value = "auto")]
599 pub backend: String,
600
601 #[arg(long, value_name = "IDS")]
604 pub gpu_devices: Option<String>,
605
606 #[arg(long, value_enum)]
608 pub layer_split_pipeline_mode: Option<crate::layer_split_pipeline::LayerSplitPipelineModeArg>,
609
610 #[arg(long)]
614 pub prompt: Option<String>,
615
616 #[arg(long)]
620 pub tokenizer: Option<PathBuf>,
621
622 #[arg(long)]
625 pub bench_mode: bool,
626
627 #[arg(long, default_value = "50")]
632 pub top_k: usize,
633
634 #[arg(long, default_value = "0.95")]
638 pub top_p: f32,
639
640 #[arg(long, default_value = "0.0")]
643 pub min_p: f32,
644
645 #[arg(long, default_value = "0.0")]
648 pub presence_penalty: f32,
649
650 #[arg(long, default_value_t = DEFAULT_CHAT_REPETITION_PENALTY)]
657 pub repeat_penalty: f32,
658
659 #[arg(long, default_value = "64")]
662 pub repeat_last_n: usize,
663
664 #[arg(long)]
666 pub seed: Option<u64>,
667
668 #[arg(long, default_value = "0.9")]
674 pub gpu_memory_utilization: f32,
675
676 #[arg(long, value_name = "BYTES")]
680 pub runtime_memory_budget_bytes: Option<std::num::NonZeroUsize>,
681
682 #[arg(long, value_name = "N")]
684 pub max_model_len: Option<usize>,
685
686 #[arg(long, value_name = "N")]
688 pub max_num_seqs: Option<usize>,
689
690 #[arg(long, value_name = "N")]
692 pub max_num_batched_tokens: Option<usize>,
693
694 #[arg(long, value_enum)]
696 pub sequence_fit_policy: Option<crate::commands::SequenceFitPolicyArg>,
697
698 #[arg(long, conflicts_with = "disable_batched_graph")]
700 pub batched_graph: bool,
701
702 #[arg(long, conflicts_with = "batched_graph")]
704 pub disable_batched_graph: bool,
705
706 #[arg(long, conflicts_with = "disable_reusable_execution")]
708 pub reusable_execution: bool,
709
710 #[arg(long, conflicts_with = "reusable_execution")]
712 pub disable_reusable_execution: bool,
713
714 #[arg(long, conflicts_with = "disable_unified_graph")]
716 pub unified_graph: bool,
717
718 #[arg(long, conflicts_with = "unified_graph")]
720 pub disable_unified_graph: bool,
721
722 #[arg(long, conflicts_with = "disable_unified_graph_layers_only")]
724 pub unified_graph_layers_only: bool,
725
726 #[arg(long, conflicts_with = "unified_graph_layers_only")]
728 pub disable_unified_graph_layers_only: bool,
729
730 #[arg(long, conflicts_with = "disable_unified_graph_lm_head_eager")]
732 pub unified_graph_lm_head_eager: bool,
733
734 #[arg(long, conflicts_with = "unified_graph_lm_head_eager")]
736 pub disable_unified_graph_lm_head_eager: bool,
737
738 #[arg(long, value_name = "DTYPE")]
743 pub kv_dtype: Option<String>,
744
745 #[arg(long, value_name = "N")]
747 pub kv_capacity: Option<usize>,
748
749 #[arg(long, value_name = "N")]
751 pub kv_max_blocks: Option<usize>,
752
753 #[arg(long)]
755 pub effective_config_json: Option<PathBuf>,
756
757 #[arg(long)]
759 pub decision_trace_jsonl: Option<PathBuf>,
760
761 #[arg(long, value_name = "DIR")]
763 pub observability_vertical_slice_out: Option<PathBuf>,
764
765 #[command(flatten)]
766 pub vnext_checkpoint: crate::commands::vnext_checkpoint::VNextCheckpointArgs,
767
768 #[arg(long, value_name = "PATH")]
770 pub profile_jsonl: Option<PathBuf>,
771
772 #[arg(long, value_enum, default_value_t = crate::observability_product::ProfileDetailArg::Off)]
774 pub profile_detail: crate::observability_product::ProfileDetailArg,
775
776 #[arg(long, value_enum)]
778 pub vnext_diagnostic_fault: Option<crate::commands::VNextDiagnosticFaultArg>,
779
780 #[arg(long, value_name = "PATH")]
782 pub memory_profile_jsonl: Option<PathBuf>,
783
784 #[arg(long, value_name = "PATH")]
786 pub scheduler_trace_jsonl: Option<PathBuf>,
787
788 #[arg(long, value_name = "DIR")]
790 pub request_dump_dir: Option<PathBuf>,
791
792 #[arg(long, default_value_t = crate::observability_product::default_profile_sample_rate())]
794 pub profile_sample_rate: f64,
795
796 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
799 pub output_format: OutputFormat,
800}
801
802pub async fn execute(cmd: RunCommand, config: CliConfig) -> Result<()> {
803 if let Some(out_dir) = cmd.observability_vertical_slice_out.as_ref() {
804 crate::observability_vertical_slice::write_observability_vertical_slice(
805 ferrum_types::ProfileEntrypoint::Run,
806 out_dir,
807 )?;
808 println!(
809 "OBSERVABILITY VERTICAL SLICE ARTIFACT: {}",
810 out_dir.display()
811 );
812 return Ok(());
813 }
814 let model = cmd.model.as_deref().ok_or_else(|| {
815 FerrumError::config(crate::source_resolver::first_success_model_help("run"))
816 })?;
817 let product_observability = crate::observability_product::ProductObservabilityConfig::new(
818 ferrum_types::ProfileEntrypoint::Run,
819 model,
820 cmd.profile_jsonl.as_ref(),
821 cmd.profile_detail,
822 cmd.memory_profile_jsonl.as_ref(),
823 cmd.scheduler_trace_jsonl.as_ref(),
824 cmd.request_dump_dir.as_ref(),
825 cmd.profile_sample_rate,
826 );
827 let memory_sampler = crate::memory_profile::ProcessMemorySampler;
828 let product_memory_enabled = product_observability.enabled();
829 let process_start_sample = product_memory_enabled
830 .then(|| memory_sampler.sample())
831 .flatten();
832 let process_start_memory = process_start_sample
833 .clone()
834 .map(crate::memory_profile::ProcessMemoryObservation::from_sample);
835 if product_observability.synthetic_no_weight_enabled() {
836 let written = crate::observability_product::write_synthetic_product_observability(
837 &product_observability,
838 )?;
839 println!(
840 "OBSERVABILITY PRODUCT ARTIFACTS: {}",
841 written
842 .iter()
843 .map(|path| path.display().to_string())
844 .collect::<Vec<_>>()
845 .join(",")
846 );
847 return Ok(());
848 }
849
850 let moe_graph_defaults = crate::runtime_env::moe_graph_default_entries(
853 &ferrum_types::RuntimeConfigSnapshot::capture_current(),
854 ferrum_types::RuntimeConfigSource::Default,
855 );
856 crate::runtime_env::materialize_runtime_env_defaults(&moe_graph_defaults);
857 crate::runtime_env::warn_if_moe_graph_needs_unbuilt_vllm_moe(
858 &ferrum_types::RuntimeConfigSnapshot::capture_current(),
859 );
860
861 let mut device = select_device(&cmd.backend)?;
864 let mut gpu_selection =
865 crate::gpu_devices::resolve_cuda_gpu_devices(cmd.gpu_devices.as_deref(), &device)?;
866 if let Some(selection) = &gpu_selection {
867 device = selection.primary_device();
868 eprintln!(
869 "{} {} ({})",
870 "CUDA GPUs:".dimmed(),
871 selection.selected_csv(),
872 selection.selected_distributed_strategy
873 );
874 }
875 let backend_initialized_sample = product_memory_enabled
876 .then(|| memory_sampler.sample())
877 .flatten();
878 let backend_initialized_memory = process_memory_observation_between(
879 process_start_sample.clone(),
880 backend_initialized_sample.clone(),
881 );
882 let mut startup_cli_runtime_entries =
883 run_startup_cli_runtime_entries(&cmd, gpu_selection.as_ref());
884 let early_runtime_config =
890 run_base_runtime_config(&config, RuntimeConfigSnapshot::capture_current());
891 let early_effective_runtime_config =
892 run_effective_runtime_config(&early_runtime_config, &startup_cli_runtime_entries);
893 let run_product_bridge = run_product_runtime_bridge(&early_effective_runtime_config);
894 let materialized_run_product_keys =
895 crate::runtime_env::materialize_runtime_env_effective(&run_product_bridge);
896 materialize_run_cli_runtime_entries(&startup_cli_runtime_entries);
897 let autosize = run_autosize_for_device(&device, cmd.gpu_memory_utilization);
898
899 let cache_dir = crate::source_resolver::hf_cache_dir(&config);
908 let resolved = crate::source_resolver::resolve_model_source_with_product_sources(
909 model,
910 &cache_dir,
911 crate::source_resolver::DownloadPolicy::AutoDownload,
912 autosize,
913 &cmd.product_sources,
914 )
915 .await?;
916 let product_input = resolved.into_product_engine_input();
917 let requested_model = product_input.requested_model.clone();
918 let model_id = product_input.public_model_id.clone();
919 let source = product_input.source;
920 let mut engine_config = product_input.engine_config;
921 let model_sources = product_input.model_sources;
922 let prepared_model = model_sources
923 .as_ref()
924 .map(crate::source_resolver::prepare_registered_product_model)
925 .transpose()?
926 .flatten();
927 let model_definition_for_config = if prepared_model.is_none() {
928 load_run_model_definition(&source, model_sources.as_deref()).await?
929 } else {
930 None
931 };
932 let model_layer_count = prepared_model
933 .as_ref()
934 .map(|prepared| prepared.descriptor().layer_count())
935 .or_else(|| {
936 model_definition_for_config
937 .as_ref()
938 .map(|definition| definition.num_hidden_layers)
939 });
940 if let (Some(selection), Some(layer_count)) = (gpu_selection.as_mut(), model_layer_count) {
941 if selection.apply_model_layer_count(layer_count)? {
942 if let Some(plan) = selection.selected_layer_split_plan.as_deref() {
943 eprintln!("{}", format!("CUDA layer split plan: {plan}").dimmed());
944 }
945 startup_cli_runtime_entries =
946 run_startup_cli_runtime_entries(&cmd, gpu_selection.as_ref());
947 materialize_run_cli_runtime_entries(&startup_cli_runtime_entries);
948 }
949 }
950 let model_chat_template = match prepared_model.as_deref() {
951 Some(prepared) => Some(crate::source_resolver::load_prepared_product_chat_template(
952 prepared,
953 )?),
954 None => match model_sources.as_deref() {
955 Some(sources) => crate::source_resolver::load_product_chat_template(sources),
956 None => crate::source_resolver::load_model_chat_template(&source.local_path),
957 },
958 };
959 let product_source_identity = prepared_model
960 .as_deref()
961 .map(|prepared| {
962 crate::source_resolver::prepared_product_source_identity(
963 prepared,
964 &requested_model,
965 &model_id,
966 model_chat_template.as_ref(),
967 )
968 })
969 .transpose()?;
970 let chat_template_options = build_chat_template_options(&cmd, model_chat_template.as_ref());
971 eprintln!("{}", format!("Loading {}...", model_id).dimmed());
972
973 let engine_model_path = source.local_path.to_string_lossy().to_string();
974
975 let device_label = format!("{device:?}");
976 eprintln!("{}", format!("Using {device_label} backend").dimmed());
977 let metal_moe_entries = crate::source_resolver::metal_gguf_moe_correctness_entries(
978 &source.local_path,
979 &device,
980 &ferrum_types::RuntimeConfigSnapshot::capture_current(),
981 ferrum_types::RuntimeConfigSource::Default,
982 );
983 crate::runtime_env::materialize_runtime_env_defaults(&metal_moe_entries);
984
985 eprintln!(
990 "{}",
991 "Loading weights to GPU... (30s+ for >10 GB models)".dimmed()
992 );
993 let load_start = std::time::Instant::now();
994 engine_config.sampling.default_params = build_sampling_params(&cmd);
995 engine_config.backend.device = device.clone();
996 engine_config.scheduler.policy = ferrum_types::SchedulingPolicy::ContinuousBatch;
997 engine_config.backend.backend_options.insert(
998 "model_path".to_string(),
999 serde_json::Value::String(engine_model_path),
1000 );
1001 let runtime_config = run_base_runtime_config(
1002 &config,
1003 runtime_config_without_keys(
1004 RuntimeConfigSnapshot::capture_current(),
1005 &materialized_run_product_keys,
1006 ),
1007 );
1008 if let Some(selection) = &gpu_selection {
1009 selection.insert_backend_options(&mut engine_config.backend.backend_options);
1010 }
1011 crate::layer_split_pipeline::insert_backend_option_from_runtime(
1012 &runtime_config,
1013 &mut engine_config.backend.backend_options,
1014 )?;
1015 let effective_runtime_config =
1016 run_effective_runtime_config(&runtime_config, &startup_cli_runtime_entries);
1017 let typed_model_capabilities = prepared_model
1018 .as_ref()
1019 .map(|prepared| prepared.model_capabilities())
1020 .transpose()?;
1021 let startup_auto_config = run_startup_auto_config(
1022 &device,
1023 typed_model_capabilities,
1024 if prepared_model.is_some() {
1025 ferrum_types::ExecutionResourceAuthority::PlanRuntime
1026 } else {
1027 ferrum_types::ExecutionResourceAuthority::LegacyEngine
1028 },
1029 model_definition_for_config.as_ref(),
1030 crate::commands::serve::model_weight_bytes_from_path(&source.local_path),
1031 effective_runtime_config,
1032 )?;
1033 crate::runtime_env::materialize_runtime_env_effective(&startup_auto_config.runtime_config);
1038 crate::commands::serve::write_startup_config_artifacts(
1039 &startup_auto_config,
1040 product_source_identity.as_ref(),
1041 cmd.effective_config_json.as_deref(),
1042 cmd.decision_trace_jsonl.as_deref(),
1043 )?;
1044 let run_budget = RunBudget::from_product_sources(
1045 cmd.tokenizer.as_deref(),
1046 model_sources.as_deref(),
1047 &source.local_path,
1048 &runtime_config,
1049 )?;
1050 engine_config
1051 .apply_runtime_config_snapshot(&startup_auto_config.runtime_config)
1052 .map_err(ferrum_types::FerrumError::config)?;
1053 let vnext_checkpoint_capture = cmd.vnext_checkpoint.to_config()?;
1054 validate_teacher_forced_checkpoint_run(&cmd, vnext_checkpoint_capture.as_ref())?;
1055 let teacher_forcing = vnext_checkpoint_capture
1056 .as_ref()
1057 .and_then(|capture| capture.teacher_forcing.clone());
1058 engine_config.runtime.vnext_checkpoint_capture = vnext_checkpoint_capture;
1059 if runtime_config_bool(&startup_auto_config.runtime_config, "FERRUM_PAGED_KV")
1060 .or_else(|| {
1061 runtime_config_bool(&startup_auto_config.runtime_config, "FERRUM_METAL_PAGED_KV")
1062 })
1063 .unwrap_or(false)
1064 {
1065 engine_config.kv_cache.cache_type = ferrum_types::KvCacheType::Paged;
1066 }
1067 let effective_kv_dtype = cmd
1068 .kv_dtype
1069 .as_deref()
1070 .or_else(|| crate::runtime_env::runtime_snapshot_value(&runtime_config, "FERRUM_KV_DTYPE"));
1071 apply_kv_dtype_override(&mut engine_config, effective_kv_dtype)?;
1072 let engine = match (prepared_model, model_sources) {
1073 (Some(prepared), _) => {
1074 ferrum_engine::create_prepared_product_engine(engine_config, prepared).await?
1075 }
1076 (None, Some(sources)) => {
1077 ferrum_engine::create_product_engine(engine_config, sources).await?
1078 }
1079 (None, None) => ferrum_engine::create_default_engine(engine_config).await?,
1080 };
1081 let model_loaded_sample = product_memory_enabled
1082 .then(|| memory_sampler.sample())
1083 .flatten();
1084 let model_loaded_memory = process_memory_observation_between(
1085 backend_initialized_sample
1086 .clone()
1087 .or_else(|| process_start_sample.clone()),
1088 model_loaded_sample.clone(),
1089 );
1090 let model_loaded_duration_us = load_start
1091 .elapsed()
1092 .as_micros()
1093 .try_into()
1094 .unwrap_or(u64::MAX);
1095 let profile_run_done_sample = product_memory_enabled
1096 .then(|| memory_sampler.sample())
1097 .flatten();
1098 let profile_run_done_memory = process_memory_observation_between(
1099 model_loaded_sample.clone(),
1100 profile_run_done_sample.clone(),
1101 );
1102 let cache_allocated_status = if product_memory_enabled {
1103 Some(engine.status().await)
1104 } else {
1105 None
1106 };
1107 let cache_allocated_sample = product_memory_enabled
1108 .then(|| memory_sampler.sample())
1109 .flatten();
1110 let cache_allocated_memory = process_memory_observation_between(
1111 profile_run_done_sample
1112 .clone()
1113 .or_else(|| model_loaded_sample.clone()),
1114 cache_allocated_sample.clone(),
1115 );
1116 eprintln!(
1117 "{}",
1118 format!(
1119 "Model loaded in {:.1}s.",
1120 load_start.elapsed().as_secs_f64()
1121 )
1122 .dimmed()
1123 );
1124 let run_session_id = Uuid::new_v4().to_string();
1125
1126 if let Some(one_shot) = cmd.prompt.clone() {
1132 let format = cmd.output_format;
1133 let plan = build_run_prompt_plan(
1134 &[],
1135 &one_shot,
1136 cmd.system.as_deref(),
1137 &model_id,
1138 model_chat_template.as_ref(),
1139 &chat_template_options,
1140 &cmd,
1141 &run_budget,
1142 )?;
1143 if let Some(teacher) = &teacher_forcing {
1144 if plan.sampling_params.max_tokens != teacher.token_count() {
1145 return Err(ferrum_types::FerrumError::config(format!(
1146 "teacher-forced checkpoint requested {} tokens, but context planning resolved max_tokens={}",
1147 teacher.token_count(),
1148 plan.sampling_params.max_tokens
1149 )));
1150 }
1151 }
1152 maybe_warn_context_shift(&plan, format);
1153 let prompt_opened_thinking = has_unclosed_thinking_block(&plan.prompt);
1154 let metadata = run_request_metadata(&plan.prompt, &chat_template_options);
1155 let prompt_chars = plan.prompt.chars().count();
1156 let request_id = RequestId(Uuid::new_v4());
1157 let request_id_text = request_id.to_string();
1158 let one_shot_history = Vec::new();
1159 if format == OutputFormat::Jsonl {
1160 emit_jsonl_ready(&run_session_id, &requested_model, &model_id, &device_label);
1161 emit_jsonl_user(
1162 &run_session_id,
1163 0,
1164 &request_id_text,
1165 0,
1166 &one_shot,
1167 &one_shot_history,
1168 );
1169 }
1170 let request = InferenceRequest {
1171 id: request_id,
1172 model_id: ferrum_types::ModelId(model_id.clone()),
1173 prompt: plan.prompt,
1174 sampling_params: plan.sampling_params.clone(),
1175 stream: format == OutputFormat::Jsonl,
1176 priority: Priority::Normal,
1177 client_id: None,
1178 session_id: None,
1179 created_at: Utc::now(),
1180 api_request: None,
1181 evidence_request: ferrum_types::InferenceEvidenceRequest {
1182 capture_engine_token_timing: product_observability
1183 .profile_detail
1184 .captures_engine_token_timing(),
1185 ..Default::default()
1186 },
1187 metadata,
1188 };
1189 let profile_request_id = request.id.to_string();
1190 let memory_before = product_observability
1191 .enabled()
1192 .then(|| memory_sampler.sample())
1193 .flatten();
1194 let start = std::time::Instant::now();
1195 let trace_tokens =
1196 crate::runtime_env::runtime_snapshot_value(&runtime_config, "FERRUM_RUN_TRACE_TOKENS")
1197 .is_some();
1198 let generation_result = match format {
1199 OutputFormat::Text => engine
1200 .infer(request)
1201 .await
1202 .map(CollectedRunGeneration::from_response),
1203 OutputFormat::Jsonl => match engine.infer_stream(request).await {
1204 Ok(stream) => {
1205 collect_run_stream(
1206 stream,
1207 trace_tokens,
1208 0,
1209 &run_session_id,
1210 0,
1211 &request_id_text,
1212 )
1213 .await
1214 }
1215 Err(error) => Err(error),
1216 },
1217 };
1218 let generation = match generation_result {
1219 Ok(generation) => generation,
1220 Err(err) => {
1221 let memory_after = product_memory_enabled
1222 .then(|| memory_sampler.sample())
1223 .flatten();
1224 let memory = process_memory_observation_between(memory_before, memory_after);
1225 let elapsed = start.elapsed().as_secs_f64();
1226 if let Err(observability_err) =
1227 crate::observability_product::write_actual_run_failure_observability(
1228 &product_observability,
1229 &crate::observability_product::ActualRunFailureObservation {
1230 request_id: profile_request_id,
1231 duration_us: (elapsed * 1_000_000.0).max(0.0) as u64,
1232 sampling_params: plan.sampling_params.clone(),
1233 prompt_token_ids: plan.prompt_token_ids.clone(),
1234 prompt_token_count: plan.prompt_tokens,
1235 prompt_chars,
1236 failure_kind: err.observability_failure_kind().to_string(),
1237 error_kind: err.observability_error_kind().to_string(),
1238 error_message: err.to_string(),
1239 memory,
1240 memory_stages: actual_run_memory_stages(
1241 product_memory_enabled,
1242 process_start_memory.clone(),
1243 backend_initialized_memory.clone(),
1244 model_loaded_memory.clone(),
1245 model_loaded_duration_us,
1246 profile_run_done_memory.clone(),
1247 cache_allocated_memory.clone(),
1248 cache_allocated_status.clone(),
1249 None,
1250 ),
1251 },
1252 )
1253 {
1254 eprintln!("failed to write run failure observability: {observability_err}");
1255 }
1256 return Err(err);
1257 }
1258 };
1259 let memory_after = product_memory_enabled
1260 .then(|| memory_sampler.sample())
1261 .flatten();
1262 let memory = process_memory_observation_between(memory_before, memory_after.clone());
1263 let CollectedRunGeneration {
1264 request_id: response_request_id,
1265 raw_text,
1266 finish_reason,
1267 usage,
1268 token_count: tokens,
1269 token_ids: output_token_ids,
1270 chunk_count,
1271 execution_evidence,
1272 } = generation;
1273 if response_request_id != request_id_text {
1274 return Err(FerrumError::internal(format!(
1275 "run response request id drift: expected {request_id_text}, got {response_request_id}"
1276 )));
1277 }
1278 let raw_response = display_response_text(&raw_text);
1279 let parsed = parse_reasoning_response_for_prompt(&raw_response, prompt_opened_thinking);
1280 let content = display_response_text(&parsed.content);
1281 let reasoning = parsed
1282 .reasoning
1283 .as_deref()
1284 .map(str::trim)
1285 .filter(|value| !value.is_empty());
1286 let bench = cmd.bench_mode;
1287 if format == OutputFormat::Text && !bench {
1288 print!("{}", raw_response);
1289 io::stdout().flush().ok();
1290 }
1291 let elapsed = start.elapsed().as_secs_f64();
1292 let tps = if elapsed > 0.0 {
1293 tokens as f64 / elapsed
1294 } else {
1295 0.0
1296 };
1297 match format {
1298 OutputFormat::Text => {
1299 if !bench {
1300 println!();
1301 }
1302 eprintln!(
1303 "{}",
1304 format!("[{tokens} tokens, {tps:.1} tok/s, {elapsed:.1}s]").dimmed()
1305 );
1306 }
1307 OutputFormat::Jsonl => {
1308 emit_jsonl_assistant(
1309 &run_session_id,
1310 0,
1311 &request_id_text,
1312 0,
1313 &content,
1314 reasoning,
1315 &one_shot_history,
1316 finish_reason,
1317 usage.as_ref(),
1318 tokens,
1319 chunk_count,
1320 &raw_response,
1321 elapsed * 1000.0,
1322 );
1323 }
1324 }
1325 let shutdown_result = engine.shutdown().await;
1326 let shutdown_after = product_memory_enabled
1327 .then(|| memory_sampler.sample())
1328 .flatten();
1329 let shutdown_memory = process_memory_observation_between(
1330 memory_after
1331 .clone()
1332 .or_else(|| model_loaded_sample.clone())
1333 .or_else(|| backend_initialized_sample.clone())
1334 .or_else(|| process_start_sample.clone()),
1335 shutdown_after,
1336 );
1337 crate::observability_product::write_actual_run_observability(
1338 &product_observability,
1339 &crate::observability_product::ActualRunObservation {
1340 request_id: profile_request_id,
1341 duration_us: (elapsed * 1_000_000.0).max(0.0) as u64,
1342 sampling_params: plan.sampling_params.clone(),
1343 prompt_token_ids: plan.prompt_token_ids.clone(),
1344 prompt_token_count: plan.prompt_tokens,
1345 output_tokens: tokens,
1346 output_token_ids,
1347 chunk_count,
1348 finish_reason: finish_reason.map(finish_reason_str).map(str::to_string),
1349 prompt_chars,
1350 response_chars: raw_response.chars().count(),
1351 response_text: raw_response,
1352 execution_evidence,
1353 memory,
1354 memory_stages: actual_run_memory_stages(
1355 product_memory_enabled,
1356 process_start_memory.clone(),
1357 backend_initialized_memory.clone(),
1358 model_loaded_memory.clone(),
1359 model_loaded_duration_us,
1360 profile_run_done_memory.clone(),
1361 cache_allocated_memory.clone(),
1362 cache_allocated_status.clone(),
1363 shutdown_memory,
1364 ),
1365 },
1366 )?;
1367 shutdown_result?;
1368 if format == OutputFormat::Jsonl {
1369 emit_jsonl_exit(&run_session_id, 0, "one_shot_complete");
1370 }
1371 return Ok(());
1372 }
1373
1374 let mut history: Vec<(String, String)> = Vec::new(); let mut history_epoch = 0usize;
1376 let mut turn = 0usize;
1377 let mut exit_reason: &str = "eof";
1378
1379 let format = cmd.output_format;
1381 match format {
1382 OutputFormat::Text => {
1383 eprintln!();
1384 eprintln!("{}", "Ready. Type your message and press Enter.".green());
1385 eprintln!(
1386 "{}",
1387 "Use /clear to reset history; /bye or Ctrl+D to exit.".dimmed()
1388 );
1389 eprintln!();
1390 }
1391 OutputFormat::Jsonl => {
1392 emit_jsonl_ready(&run_session_id, &requested_model, &model_id, &device_label);
1393 }
1394 }
1395
1396 let stdin_handle = io::stdin();
1399 let stdin_is_tty = stdin_handle.is_terminal();
1400 let term = stdin_is_tty.then(Term::stdout);
1401 let mut stdin = stdin_handle.lock();
1402
1403 loop {
1404 if stdin_is_tty {
1405 print!("{} ", ">>>".bright_green().bold());
1407 io::stdout().flush().unwrap();
1408 }
1409
1410 let mut input = String::new();
1412 match read_repl_input_line(term.as_ref(), &mut stdin, &mut input) {
1413 Ok(0) => break, Ok(_) => {
1415 let input = input.trim();
1416 if input.is_empty() {
1417 continue;
1418 }
1419 if input == "/bye" || input == "exit" || input == "quit" {
1420 exit_reason = match input {
1421 "/bye" => "bye",
1422 "exit" => "exit",
1423 "quit" => "quit",
1424 _ => "command",
1425 };
1426 break;
1427 }
1428 if input == "/clear" {
1429 let before = history_evidence(&history);
1430 history.clear();
1431 history_epoch += 1;
1432 turn = 0;
1433 match format {
1434 OutputFormat::Text => {
1435 eprintln!("{}", "History cleared.".dimmed());
1436 }
1437 OutputFormat::Jsonl => {
1438 let record = serde_json::json!({
1439 "schema_version": RUN_JSONL_SCHEMA_VERSION,
1440 "event": "history_reset",
1441 "session_id": run_session_id,
1442 "history_epoch": history_epoch,
1443 "turn": turn,
1444 "history_before": before,
1445 "history_after": history_evidence(&history),
1446 });
1447 emit_jsonl_record(&record);
1448 }
1449 }
1450 continue;
1451 }
1452
1453 let plan = build_run_prompt_plan(
1454 &history,
1455 input,
1456 cmd.system.as_deref(),
1457 &model_id,
1458 model_chat_template.as_ref(),
1459 &chat_template_options,
1460 &cmd,
1461 &run_budget,
1462 )?;
1463 maybe_warn_context_shift(&plan, format);
1464 let prompt_opened_thinking = has_unclosed_thinking_block(&plan.prompt);
1465 let observability_sampling_params =
1466 product_memory_enabled.then(|| plan.sampling_params.clone());
1467 let prompt_token_ids = plan.prompt_token_ids;
1468 let prompt_token_count = plan.prompt_tokens;
1469 let prompt_chars = plan.prompt.chars().count();
1470 let metadata = run_request_metadata(&plan.prompt, &chat_template_options);
1471 let request_id = RequestId(Uuid::new_v4());
1472 let expected_request_id = request_id.clone();
1473 let request_id_text = request_id.to_string();
1474 if format == OutputFormat::Jsonl {
1475 emit_jsonl_user(
1476 &run_session_id,
1477 history_epoch,
1478 &request_id_text,
1479 turn,
1480 input,
1481 &history,
1482 );
1483 }
1484 let request = InferenceRequest {
1486 id: request_id,
1487 model_id: ferrum_types::ModelId(model_id.clone()),
1488 prompt: plan.prompt,
1489 sampling_params: plan.sampling_params,
1490 stream: true,
1491 priority: Priority::Normal,
1492 client_id: None,
1493 session_id: None,
1494 created_at: Utc::now(),
1495 api_request: None,
1496 evidence_request: ferrum_types::InferenceEvidenceRequest {
1497 capture_engine_token_timing: product_observability
1498 .profile_detail
1499 .captures_engine_token_timing(),
1500 ..Default::default()
1501 },
1502 metadata,
1503 };
1504
1505 let memory_before = product_memory_enabled
1506 .then(|| memory_sampler.sample())
1507 .flatten();
1508 let memory_stages = if product_memory_enabled && history_epoch == 0 && turn == 0 {
1509 let mut stages = actual_run_memory_stages(
1510 product_memory_enabled,
1511 process_start_memory.clone(),
1512 backend_initialized_memory.clone(),
1513 model_loaded_memory.clone(),
1514 model_loaded_duration_us,
1515 profile_run_done_memory.clone(),
1516 cache_allocated_memory.clone(),
1517 cache_allocated_status.clone(),
1518 None,
1519 );
1520 stages.retain(|stage| stage.stage != "shutdown");
1521 stages
1522 } else {
1523 Vec::new()
1524 };
1525 let start = std::time::Instant::now();
1526 let trace_tokens = crate::runtime_env::runtime_snapshot_value(
1527 &runtime_config,
1528 "FERRUM_RUN_TRACE_TOKENS",
1529 )
1530 .is_some();
1531 let generation_result = match format {
1532 OutputFormat::Text => match engine.infer_stream(request).await {
1533 Ok(stream) => {
1534 collect_run_text_stream(
1535 stream,
1536 trace_tokens,
1537 turn,
1538 &expected_request_id,
1539 stdin_is_tty,
1540 product_memory_enabled,
1541 )
1542 .await
1543 }
1544 Err(error) => Err(error),
1545 },
1546 OutputFormat::Jsonl => match engine.infer_stream(request).await {
1547 Ok(stream) => {
1548 collect_run_stream(
1549 stream,
1550 trace_tokens,
1551 turn,
1552 &run_session_id,
1553 history_epoch,
1554 &request_id_text,
1555 )
1556 .await
1557 }
1558 Err(error) => Err(error),
1559 },
1560 };
1561 let generation = match generation_result {
1562 Ok(generation) => generation,
1563 Err(error) => {
1564 let memory_after = product_memory_enabled
1565 .then(|| memory_sampler.sample())
1566 .flatten();
1567 let memory =
1568 process_memory_observation_between(memory_before, memory_after);
1569 let elapsed = start.elapsed().as_secs_f64();
1570 if product_memory_enabled {
1571 if let Err(observability_error) =
1572 crate::observability_product::write_actual_run_failure_observability(
1573 &product_observability,
1574 &crate::observability_product::ActualRunFailureObservation {
1575 request_id: request_id_text,
1576 duration_us: (elapsed * 1_000_000.0).max(0.0) as u64,
1577 sampling_params: observability_sampling_params.expect(
1578 "enabled run observability must capture sampling parameters",
1579 ),
1580 prompt_token_ids,
1581 prompt_token_count,
1582 prompt_chars,
1583 failure_kind: error
1584 .observability_failure_kind()
1585 .to_string(),
1586 error_kind: error.observability_error_kind().to_string(),
1587 error_message: error.to_string(),
1588 memory,
1589 memory_stages,
1590 },
1591 )
1592 {
1593 eprintln!(
1594 "failed to write interactive run failure observability: {observability_error}"
1595 );
1596 }
1597 }
1598 return Err(error);
1599 }
1600 };
1601 let memory_after = product_memory_enabled
1602 .then(|| memory_sampler.sample())
1603 .flatten();
1604 let memory = process_memory_observation_between(memory_before, memory_after);
1605 let CollectedRunGeneration {
1606 request_id: response_request_id,
1607 raw_text,
1608 finish_reason,
1609 usage,
1610 token_count,
1611 token_ids: output_token_ids,
1612 chunk_count,
1613 execution_evidence,
1614 } = generation;
1615 if response_request_id != request_id_text {
1616 return Err(FerrumError::internal(format!(
1617 "run response request id drift: expected {request_id_text}, got {response_request_id}"
1618 )));
1619 }
1620 let raw_response = display_response_text(&raw_text);
1621 let (clean_response, reasoning) = match format {
1622 OutputFormat::Text => (raw_response.clone(), None),
1623 OutputFormat::Jsonl => {
1624 let parsed = parse_reasoning_response_for_prompt(
1625 &raw_response,
1626 prompt_opened_thinking,
1627 );
1628 (
1629 display_response_text(&parsed.content),
1630 parsed
1631 .reasoning
1632 .map(|value| value.trim().to_string())
1633 .filter(|value| !value.is_empty()),
1634 )
1635 }
1636 };
1637
1638 let elapsed = start.elapsed();
1639 let elapsed_s = elapsed.as_secs_f64();
1640 let tps = if elapsed_s > 0.0 {
1641 token_count as f64 / elapsed_s
1642 } else {
1643 0.0
1644 };
1645
1646 match format {
1647 OutputFormat::Text => {
1648 println!();
1649 eprintln!(
1650 "{}",
1651 format!("[{token_count} tokens, {tps:.1} tok/s, {elapsed_s:.1}s]")
1652 .dimmed()
1653 );
1654 eprintln!();
1655 }
1656 OutputFormat::Jsonl => {
1657 emit_jsonl_assistant(
1658 &run_session_id,
1659 history_epoch,
1660 &request_id_text,
1661 turn,
1662 &clean_response,
1663 reasoning.as_deref(),
1664 &history,
1665 finish_reason,
1666 usage.as_ref(),
1667 token_count,
1668 chunk_count,
1669 &raw_response,
1670 elapsed_s * 1000.0,
1671 );
1672 }
1673 }
1674
1675 if product_memory_enabled {
1676 crate::observability_product::write_actual_run_observability(
1677 &product_observability,
1678 &crate::observability_product::ActualRunObservation {
1679 request_id: request_id_text.clone(),
1680 duration_us: (elapsed_s * 1_000_000.0).max(0.0) as u64,
1681 sampling_params: observability_sampling_params.expect(
1682 "enabled run observability must capture sampling parameters",
1683 ),
1684 prompt_token_ids,
1685 prompt_token_count,
1686 output_tokens: token_count,
1687 output_token_ids,
1688 chunk_count,
1689 finish_reason: finish_reason.map(finish_reason_str).map(str::to_string),
1690 prompt_chars,
1691 response_chars: raw_response.chars().count(),
1692 response_text: raw_response.clone(),
1693 execution_evidence,
1694 memory,
1695 memory_stages,
1696 },
1697 )?;
1698 }
1699
1700 if !stdin_is_tty {
1702 io::stdout().flush().ok();
1703 io::stderr().flush().ok();
1704 }
1705
1706 history.push(("user".to_string(), input.to_string()));
1708 if !raw_response.is_empty() {
1709 history.push(("assistant".to_string(), raw_response));
1710 }
1711
1712 while history.len() > 10 {
1714 history.remove(0);
1715 }
1716 turn += 1;
1717 }
1718 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
1719 exit_reason = "interrupt";
1720 break;
1721 }
1722 Err(e) => {
1723 eprintln!("{} {}", "Error reading input:".red(), e);
1724 exit_reason = "read_error";
1725 break;
1726 }
1727 }
1728 }
1729
1730 match format {
1731 OutputFormat::Text => {
1732 eprintln!("{}", "Goodbye!".bright_yellow());
1733 }
1734 OutputFormat::Jsonl => {
1735 emit_jsonl_exit(&run_session_id, history_epoch, exit_reason);
1736 }
1737 }
1738 engine.shutdown().await?;
1739 Ok(())
1740}
1741
1742fn process_memory_observation_between(
1743 before: Option<crate::memory_profile::ProcessMemorySample>,
1744 after: Option<crate::memory_profile::ProcessMemorySample>,
1745) -> Option<crate::memory_profile::ProcessMemoryObservation> {
1746 after.map(|after| crate::memory_profile::ProcessMemoryObservation::from_samples(before, after))
1747}
1748
1749fn actual_run_memory_stages(
1750 enabled: bool,
1751 process_start_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1752 backend_initialized_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1753 model_loaded_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1754 model_loaded_duration_us: u64,
1755 profile_run_done_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1756 cache_allocated_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1757 cache_allocated_status: Option<ferrum_types::EngineStatus>,
1758 shutdown_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1759) -> Vec<crate::observability_product::ActualMemoryStageObservation> {
1760 if !enabled {
1761 return Vec::new();
1762 }
1763 let profile_run_done = crate::observability_product::ActualMemoryStageObservation::new(
1764 "actual_run_profile_run_done",
1765 "profile_run_done",
1766 None,
1767 profile_run_done_memory,
1768 )
1769 .with_profile_run_status(
1770 false,
1771 "not_configured",
1772 "product_basic_profile_does_not_execute_extra_warmup",
1773 );
1774 let mut cache_allocated = crate::observability_product::ActualMemoryStageObservation::new(
1775 "actual_run_cache_allocated",
1776 "cache_allocated",
1777 None,
1778 cache_allocated_memory,
1779 );
1780 if let Some(status) = cache_allocated_status.as_ref() {
1781 cache_allocated = cache_allocated.with_engine_cache_status(status);
1782 }
1783 vec![
1784 crate::observability_product::ActualMemoryStageObservation::new(
1785 "actual_run_process_start",
1786 "process_start",
1787 None,
1788 process_start_memory,
1789 ),
1790 crate::observability_product::ActualMemoryStageObservation::new(
1791 "actual_run_backend_initialized",
1792 "backend_initialized",
1793 None,
1794 backend_initialized_memory,
1795 ),
1796 crate::observability_product::ActualMemoryStageObservation::new(
1797 "actual_run_model_loaded",
1798 "model_loaded",
1799 Some(model_loaded_duration_us),
1800 model_loaded_memory,
1801 ),
1802 profile_run_done,
1803 cache_allocated,
1804 crate::observability_product::ActualMemoryStageObservation::new(
1805 "actual_run_shutdown",
1806 "shutdown",
1807 None,
1808 shutdown_memory,
1809 ),
1810 ]
1811}
1812
1813#[cfg(unix)]
1814fn read_repl_input_line<R: BufRead + AsRawFd>(
1815 term: Option<&Term>,
1816 stdin: &mut R,
1817 input: &mut String,
1818) -> io::Result<usize> {
1819 if let Some(term) = term {
1820 let Some(line) = read_tty_input_line(term, stdin)? else {
1821 return Ok(0);
1822 };
1823 input.push_str(&line);
1824 return Ok(input.len().max(1));
1825 }
1826 stdin.read_line(input)
1827}
1828
1829#[cfg(not(unix))]
1830fn read_repl_input_line<R: BufRead>(
1831 term: Option<&Term>,
1832 stdin: &mut R,
1833 input: &mut String,
1834) -> io::Result<usize> {
1835 if let Some(term) = term {
1836 let Some(line) = read_tty_input_line(term)? else {
1837 return Ok(0);
1838 };
1839 input.push_str(&line);
1840 return Ok(input.len().max(1));
1841 }
1842 stdin.read_line(input)
1843}
1844
1845#[cfg(unix)]
1846struct RawModeGuard {
1847 fd: RawFd,
1848 original: libc::termios,
1849}
1850
1851#[cfg(unix)]
1852impl RawModeGuard {
1853 fn new(fd: RawFd) -> io::Result<Self> {
1854 let mut termios = mem::MaybeUninit::uninit();
1855 if unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) } != 0 {
1856 return Err(io::Error::last_os_error());
1857 }
1858 let original = unsafe { termios.assume_init() };
1859 let mut raw = original;
1860 unsafe { libc::cfmakeraw(&mut raw) };
1861 raw.c_oflag = original.c_oflag;
1862 if unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &raw) } != 0 {
1863 return Err(io::Error::last_os_error());
1864 }
1865 Ok(Self { fd, original })
1866 }
1867}
1868
1869#[cfg(unix)]
1870impl Drop for RawModeGuard {
1871 fn drop(&mut self) {
1872 unsafe {
1873 libc::tcsetattr(self.fd, libc::TCSADRAIN, &self.original);
1874 }
1875 }
1876}
1877
1878#[cfg(unix)]
1879fn read_tty_input_line<R: AsRawFd>(term: &Term, stdin: &mut R) -> io::Result<Option<String>> {
1880 let _raw_mode = RawModeGuard::new(stdin.as_raw_fd())?;
1881 let mut chars: Vec<char> = Vec::new();
1882 loop {
1883 match term.read_key_raw()? {
1884 Key::Backspace => {
1885 if let Some(ch) = chars.pop() {
1886 let width = measure_text_width(&ch.to_string());
1887 if width > 0 {
1888 term.clear_chars(width)?;
1889 }
1890 term.flush()?;
1891 }
1892 }
1893 Key::Char('\u{4}') => {
1894 term.write_str("\n")?;
1895 term.flush()?;
1896 if chars.is_empty() {
1897 return Ok(None);
1898 }
1899 break;
1900 }
1901 Key::CtrlC | Key::Char('\u{3}') => {
1902 term.write_str("^C\n")?;
1903 term.flush()?;
1904 return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
1905 }
1906 Key::Enter => {
1907 term.write_str("\n")?;
1908 term.flush()?;
1909 break;
1910 }
1911 Key::Char(ch) if !ch.is_ascii_control() => {
1912 chars.push(ch);
1913 term.write_str(&ch.to_string())?;
1914 term.flush()?;
1915 }
1916 _ => {}
1917 }
1918 }
1919 Ok(Some(chars.into_iter().collect()))
1920}
1921
1922#[cfg(not(unix))]
1923fn read_tty_input_line(term: &Term) -> io::Result<Option<String>> {
1924 let mut chars: Vec<char> = Vec::new();
1925 loop {
1926 match term.read_key()? {
1927 Key::Backspace => {
1928 if let Some(ch) = chars.pop() {
1929 let width = measure_text_width(&ch.to_string());
1930 if width > 0 {
1931 term.clear_chars(width)?;
1932 }
1933 term.flush()?;
1934 }
1935 }
1936 Key::Char('\u{4}') => {
1937 term.write_str("\n")?;
1938 term.flush()?;
1939 if chars.is_empty() {
1940 return Ok(None);
1941 }
1942 break;
1943 }
1944 Key::CtrlC | Key::Char('\u{3}') => {
1945 term.write_str("^C\n")?;
1946 term.flush()?;
1947 return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
1948 }
1949 Key::Enter => {
1950 term.write_str("\n")?;
1951 term.flush()?;
1952 break;
1953 }
1954 Key::Char(ch) if !ch.is_ascii_control() => {
1955 chars.push(ch);
1956 term.write_str(&ch.to_string())?;
1957 term.flush()?;
1958 }
1959 _ => {}
1960 }
1961 }
1962 Ok(Some(chars.into_iter().collect()))
1963}
1964
1965fn start_first_token_indicator(enabled: bool) -> Option<ProgressBar> {
1966 if !enabled {
1967 return None;
1968 }
1969 let progress = ProgressBar::new_spinner();
1970 let style = ProgressStyle::with_template("{spinner} Working ({elapsed})")
1971 .unwrap_or_else(|_| ProgressStyle::default_spinner());
1972 progress.set_style(style);
1973 progress.enable_steady_tick(std::time::Duration::from_millis(120));
1974 progress.tick();
1975 Some(progress)
1976}
1977
1978fn clear_first_token_indicator(progress: &mut Option<ProgressBar>) {
1979 if let Some(progress) = progress.take() {
1980 progress.finish_and_clear();
1981 }
1982}
1983
1984fn runtime_config_bool(snapshot: &RuntimeConfigSnapshot, key: &str) -> Option<bool> {
1985 crate::runtime_env::runtime_snapshot_value(snapshot, key).map(|value| {
1986 matches!(
1987 value.trim().to_ascii_lowercase().as_str(),
1988 "" | "1" | "true" | "yes" | "on"
1989 )
1990 })
1991}
1992
1993fn run_autosize_for_device(
1994 device: &ferrum_types::Device,
1995 gpu_memory_utilization: f32,
1996) -> Option<(crate::gpu_mem_autosize::AutoSizeProfile, f32)> {
1997 match device {
1998 ferrum_types::Device::CPU => None,
1999 _ => Some((
2000 crate::gpu_mem_autosize::AutoSizeProfile::Chat,
2001 gpu_memory_utilization,
2002 )),
2003 }
2004}
2005
2006fn build_sampling_params(cmd: &RunCommand) -> SamplingParams {
2007 let greedy = cmd.temperature <= 0.0;
2008 let mut stop_sequences = vec![
2009 "<|im_end|>".to_string(),
2010 "</s>".to_string(),
2011 "<|endoftext|>".to_string(),
2012 ];
2013 stop_sequences.extend(cmd.stop.iter().filter(|stop| !stop.is_empty()).cloned());
2014 SamplingParams {
2015 max_tokens: cmd.max_tokens as usize,
2016 temperature: cmd.temperature,
2017 top_p: if greedy { 1.0 } else { cmd.top_p },
2018 top_k: if greedy || cmd.top_k == 0 {
2019 None
2020 } else {
2021 Some(cmd.top_k)
2022 },
2023 min_p: (cmd.min_p != 0.0).then_some(cmd.min_p),
2024 presence_penalty: cmd.presence_penalty,
2025 repetition_penalty: cmd.repeat_penalty,
2026 stop_sequences,
2027 seed: cmd.seed,
2028 ..Default::default()
2029 }
2030}
2031
2032fn validate_teacher_forced_checkpoint_run(
2033 cmd: &RunCommand,
2034 capture: Option<&ferrum_types::VNextCheckpointCaptureConfig>,
2035) -> Result<()> {
2036 let Some(teacher) = capture.and_then(|capture| capture.teacher_forcing.as_ref()) else {
2037 return Ok(());
2038 };
2039 if cmd.prompt.is_none() {
2040 return Err(ferrum_types::FerrumError::config(
2041 "teacher-forced checkpoint capture requires one-shot --prompt",
2042 ));
2043 }
2044 if cmd.max_tokens as usize != teacher.token_count() {
2045 return Err(ferrum_types::FerrumError::config(format!(
2046 "teacher-forced checkpoint requires --max-tokens {}, got {}",
2047 teacher.token_count(),
2048 cmd.max_tokens
2049 )));
2050 }
2051 if cmd.max_num_seqs != Some(1) {
2052 return Err(ferrum_types::FerrumError::config(
2053 "teacher-forced checkpoint capture requires --max-num-seqs 1",
2054 ));
2055 }
2056 if cmd.temperature != 0.0
2057 || cmd.top_k != 0
2058 || cmd.top_p != 1.0
2059 || cmd.min_p != 0.0
2060 || cmd.presence_penalty != 0.0
2061 || cmd.repeat_penalty != 1.0
2062 || !cmd.stop.is_empty()
2063 {
2064 return Err(ferrum_types::FerrumError::config(
2065 "teacher-forced checkpoint capture requires unpenalized greedy settings: \
2066 --temperature 0 --top-k 0 --top-p 1 --min-p 0 \
2067 --presence-penalty 0 --repeat-penalty 1 and no --stop",
2068 ));
2069 }
2070 Ok(())
2071}
2072
2073fn sampling_params_for_prompt(mut sampling_params: SamplingParams, prompt: &str) -> SamplingParams {
2074 if has_unclosed_thinking_block(prompt) {
2075 sampling_params.response_completion_boundary =
2076 ResponseCompletionBoundary::AfterDelimiterAndPayload {
2077 delimiter: THINK_END_TAG.to_string(),
2078 alternate_envelope: None,
2079 };
2080 }
2081 sampling_params
2082}
2083
2084fn build_chat_template_options(
2085 cmd: &RunCommand,
2086 model_template: Option<&ModelChatTemplate>,
2087) -> ChatTemplateOptions {
2088 let mut options = ChatTemplateOptions::default_for_template(model_template);
2089 if cmd.enable_thinking {
2090 options.enable_thinking = Some(true);
2091 } else if cmd.disable_thinking {
2092 options.enable_thinking = Some(false);
2093 }
2094 options
2095}
2096
2097fn build_run_prompt_plan(
2098 history: &[(String, String)],
2099 user_input: &str,
2100 system: Option<&str>,
2101 model_id: &str,
2102 model_template: Option<&ModelChatTemplate>,
2103 chat_template_options: &ChatTemplateOptions,
2104 cmd: &RunCommand,
2105 budget: &RunBudget,
2106) -> Result<RunPromptPlan> {
2107 let base_sampling = build_sampling_params(cmd);
2108
2109 if cmd.no_context_shift {
2110 let prompt = build_chat_prompt(
2111 history,
2112 user_input,
2113 system,
2114 model_id,
2115 model_template,
2116 chat_template_options,
2117 )?;
2118 let prompt_tokenization = budget.prompt_tokenization(&prompt);
2119 let prompt_tokens = prompt_tokenization.token_count;
2120 if !fits_kv_budget(&base_sampling, prompt_tokens, budget.kv_capacity) {
2121 return Err(FerrumError::invalid_request(format!(
2122 "This model context is limited to {} tokens, but this turn needs {} input tokens + {} output tokens. Reduce --max-tokens, use /clear, or shorten the prompt.",
2123 budget.kv_capacity.unwrap_or(0),
2124 prompt_tokens.unwrap_or(0),
2125 base_sampling.max_tokens,
2126 )));
2127 }
2128
2129 let sampling_params = sampling_params_for_prompt(base_sampling, &prompt);
2130 return Ok(RunPromptPlan {
2131 prompt,
2132 sampling_params,
2133 prompt_token_ids: prompt_tokenization.token_ids,
2134 prompt_tokens,
2135 kv_capacity: budget.kv_capacity,
2136 dropped_history_messages: 0,
2137 dropped_history_turns: 0,
2138 max_tokens_clamped_from: None,
2139 });
2140 }
2141
2142 let mut history_start = 0usize;
2143 loop {
2144 let prompt = build_chat_prompt(
2145 &history[history_start..],
2146 user_input,
2147 system,
2148 model_id,
2149 model_template,
2150 chat_template_options,
2151 )?;
2152 let prompt_tokenization = budget.prompt_tokenization(&prompt);
2153 let prompt_tokens = prompt_tokenization.token_count;
2154
2155 let Some(kv_capacity) = budget.kv_capacity else {
2156 let sampling_params = sampling_params_for_prompt(base_sampling, &prompt);
2157 return Ok(RunPromptPlan {
2158 prompt,
2159 sampling_params,
2160 prompt_token_ids: prompt_tokenization.token_ids,
2161 prompt_tokens,
2162 kv_capacity: None,
2163 dropped_history_messages: history_start,
2164 dropped_history_turns: count_user_turns(&history[..history_start]),
2165 max_tokens_clamped_from: None,
2166 });
2167 };
2168 let Some(prompt_tokens) = prompt_tokens else {
2169 let sampling_params = sampling_params_for_prompt(base_sampling, &prompt);
2170 return Ok(RunPromptPlan {
2171 prompt,
2172 sampling_params,
2173 prompt_token_ids: prompt_tokenization.token_ids,
2174 prompt_tokens: None,
2175 kv_capacity: Some(kv_capacity),
2176 dropped_history_messages: history_start,
2177 dropped_history_turns: count_user_turns(&history[..history_start]),
2178 max_tokens_clamped_from: None,
2179 });
2180 };
2181
2182 if prompt_tokens < kv_capacity {
2183 let remaining = kv_capacity - prompt_tokens;
2184 let mut sampling_params = base_sampling.clone();
2185 let max_tokens_clamped_from = if sampling_params.max_tokens > remaining {
2186 let old = sampling_params.max_tokens;
2187 sampling_params.max_tokens = remaining;
2188 Some(old)
2189 } else {
2190 None
2191 };
2192 let sampling_params = sampling_params_for_prompt(sampling_params, &prompt);
2193 return Ok(RunPromptPlan {
2194 prompt,
2195 sampling_params,
2196 prompt_token_ids: prompt_tokenization.token_ids,
2197 prompt_tokens: Some(prompt_tokens),
2198 kv_capacity: Some(kv_capacity),
2199 dropped_history_messages: history_start,
2200 dropped_history_turns: count_user_turns(&history[..history_start]),
2201 max_tokens_clamped_from,
2202 });
2203 }
2204
2205 if history_start >= history.len() {
2206 return Err(FerrumError::invalid_request(format!(
2207 "This model context is limited to {kv_capacity} tokens, but the current turn needs {prompt_tokens} input tokens before generation. Use a shorter prompt or increase KV capacity.",
2208 )));
2209 }
2210 history_start = next_context_shift_history_start(history, history_start);
2211 }
2212}
2213
2214fn next_context_shift_history_start(history: &[(String, String)], start: usize) -> usize {
2215 if start + 1 < history.len()
2216 && history[start].0 == "user"
2217 && history[start + 1].0 == "assistant"
2218 {
2219 start + 2
2220 } else {
2221 start + 1
2222 }
2223}
2224
2225fn count_user_turns(history: &[(String, String)]) -> usize {
2226 history.iter().filter(|(role, _)| role == "user").count()
2227}
2228
2229fn maybe_warn_context_shift(plan: &RunPromptPlan, format: OutputFormat) {
2230 if format != OutputFormat::Text {
2231 return;
2232 }
2233 if plan.dropped_history_messages == 0 && plan.max_tokens_clamped_from.is_none() {
2234 return;
2235 }
2236
2237 let prompt_tokens = plan
2238 .prompt_tokens
2239 .map(|value| value.to_string())
2240 .unwrap_or_else(|| "?".to_string());
2241 let kv_capacity = plan
2242 .kv_capacity
2243 .map(|value| value.to_string())
2244 .unwrap_or_else(|| "?".to_string());
2245 let mut parts = Vec::new();
2246 if plan.dropped_history_messages > 0 {
2247 parts.push(format!(
2248 "dropped {} old message(s) / {} turn(s)",
2249 plan.dropped_history_messages, plan.dropped_history_turns
2250 ));
2251 }
2252 if let Some(old) = plan.max_tokens_clamped_from {
2253 parts.push(format!(
2254 "max_tokens {} -> {}",
2255 old, plan.sampling_params.max_tokens
2256 ));
2257 }
2258
2259 eprintln!(
2260 "{}",
2261 format!(
2262 "[context-shift] {} (prompt_tokens={}, kv_capacity={})",
2263 parts.join("; "),
2264 prompt_tokens,
2265 kv_capacity
2266 )
2267 .dimmed()
2268 );
2269}
2270
2271fn fits_kv_budget(
2272 base: &SamplingParams,
2273 prompt_tokens: Option<usize>,
2274 kv_capacity: Option<usize>,
2275) -> bool {
2276 let (Some(prompt_tokens), Some(kv_capacity)) = (prompt_tokens, kv_capacity) else {
2277 return true;
2278 };
2279 prompt_tokens < kv_capacity && prompt_tokens + base.max_tokens <= kv_capacity
2280}
2281
2282fn discover_run_tokenizer_path(source_path: &Path) -> Option<PathBuf> {
2283 if source_path.is_file()
2284 && source_path
2285 .extension()
2286 .map(|e| e.eq_ignore_ascii_case("gguf"))
2287 .unwrap_or(false)
2288 {
2289 return ferrum_models::gguf_engine_loader::auto_discover_tokenizer_path(source_path);
2290 }
2291 let tokenizer = source_path.join("tokenizer.json");
2292 tokenizer.is_file().then_some(tokenizer)
2293}
2294
2295pub fn select_device(backend: &str) -> Result<ferrum_types::Device> {
2296 match backend.trim().to_lowercase().as_str() {
2297 "cpu" => Ok(ferrum_types::Device::CPU),
2298 "metal" => {
2299 #[cfg(all(target_os = "macos", feature = "metal"))]
2300 {
2301 return Ok(ferrum_types::Device::Metal);
2302 }
2303 #[cfg(not(all(target_os = "macos", feature = "metal")))]
2304 {
2305 Err(FerrumError::config(
2306 "requested backend 'metal' but this ferrum binary was not built with Metal support; use --backend auto/cpu or build with the metal feature",
2307 ))
2308 }
2309 }
2310 "cuda" => {
2311 #[cfg(feature = "cuda")]
2312 {
2313 return Ok(ferrum_types::Device::CUDA(0));
2314 }
2315 #[cfg(not(feature = "cuda"))]
2316 {
2317 Err(FerrumError::config(
2318 "requested backend 'cuda' but this ferrum binary was not built with CUDA support; use --backend auto/cpu or build with the cuda feature",
2319 ))
2320 }
2321 }
2322 "auto" => {
2323 #[cfg(all(target_os = "macos", feature = "metal"))]
2324 {
2325 return Ok(ferrum_types::Device::Metal);
2326 }
2327 #[cfg(feature = "cuda")]
2328 {
2329 return Ok(ferrum_types::Device::CUDA(0));
2330 }
2331 #[allow(unreachable_code)]
2332 Ok(ferrum_types::Device::CPU)
2333 }
2334 other => Err(FerrumError::config(format!(
2335 "unknown backend {other:?}; expected one of: auto, cpu, metal, cuda"
2336 ))),
2337 }
2338}
2339
2340fn build_chat_prompt(
2341 history: &[(String, String)],
2342 user_input: &str,
2343 system: Option<&str>,
2344 model_id: &str,
2345 model_template: Option<&ModelChatTemplate>,
2346 chat_template_options: &ChatTemplateOptions,
2347) -> Result<String> {
2348 let mut messages = Vec::new();
2349 if let Some(sys) = system {
2350 messages.push(PromptMessage::new("system", sys));
2351 }
2352 for (role, content) in history {
2353 messages.push(PromptMessage::new(role, content));
2354 }
2355 messages.push(PromptMessage::new("user", user_input));
2356 ferrum_server::chat_template::render_prompt_messages_with_options(
2357 &messages,
2358 model_id,
2359 model_template,
2360 chat_template_options,
2361 )
2362}
2363
2364async fn load_run_model_definition(
2365 source: &ResolvedModelSource,
2366 product_sources: Option<&ferrum_models::vnext::ProductionModelSourceBundle>,
2367) -> Result<Option<ferrum_models::ModelDefinition>> {
2368 if let Some(sources) = product_sources {
2369 let mut config_manager = ferrum_models::ConfigManager::new();
2370 return config_manager
2371 .load_from_bytes(sources.config_json())
2372 .map(Some);
2373 }
2374 if source.format != ModelFormat::SafeTensors {
2375 return Ok(None);
2376 }
2377 let mut config_manager = ferrum_models::ConfigManager::new();
2378 Ok(Some(
2379 config_manager.load_from_path(&source.local_path).await?,
2380 ))
2381}
2382
2383fn run_effective_runtime_config(
2384 runtime_config: &RuntimeConfigSnapshot,
2385 cli_runtime_entries: &[RuntimeConfigEntry],
2386) -> RuntimeConfigSnapshot {
2387 let mut snapshot = runtime_config.clone();
2388 for entry in cli_runtime_entries {
2389 snapshot.upsert_entry(entry.clone());
2390 }
2391 snapshot
2392}
2393
2394fn run_base_runtime_config(
2395 config: &CliConfig,
2396 env_snapshot: RuntimeConfigSnapshot,
2397) -> RuntimeConfigSnapshot {
2398 let mut config_entries = run_product_default_runtime_entries();
2399 config_entries.extend(config.runtime.runtime_config_entries());
2400 crate::commands::serve::merge_runtime_config_sources(config_entries, env_snapshot, Vec::new())
2401}
2402
2403const RUN_PRODUCT_RUNTIME_KEYS: [&str; 2] = [
2404 "FERRUM_PAGED_MAX_SEQS",
2405 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS",
2406];
2407
2408fn run_product_default_runtime_entries() -> Vec<RuntimeConfigEntry> {
2409 vec![
2410 RuntimeConfigEntry::new("FERRUM_PAGED_MAX_SEQS", "1", RuntimeConfigSource::Default),
2411 RuntimeConfigEntry::new(
2412 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS",
2413 "1",
2414 RuntimeConfigSource::Default,
2415 ),
2416 ]
2417}
2418
2419fn run_product_runtime_bridge(snapshot: &RuntimeConfigSnapshot) -> RuntimeConfigSnapshot {
2420 RuntimeConfigSnapshot::from_entries(
2421 snapshot
2422 .entries
2423 .iter()
2424 .filter(|entry| RUN_PRODUCT_RUNTIME_KEYS.contains(&entry.key.as_str()))
2425 .cloned()
2426 .collect::<Vec<_>>(),
2427 )
2428}
2429
2430fn runtime_config_without_keys(
2431 mut snapshot: RuntimeConfigSnapshot,
2432 keys: &[String],
2433) -> RuntimeConfigSnapshot {
2434 snapshot
2435 .entries
2436 .retain(|entry| !keys.iter().any(|key| key == &entry.key));
2437 snapshot
2438}
2439
2440fn run_startup_cli_runtime_entries(
2441 cmd: &RunCommand,
2442 gpu_selection: Option<&crate::gpu_devices::GpuDeviceSelection>,
2443) -> Vec<RuntimeConfigEntry> {
2444 let mut entries = Vec::new();
2445 entries.push(RuntimeConfigEntry::new(
2446 "FERRUM_PROFILE_DETAIL",
2447 cmd.profile_detail.as_str(),
2448 RuntimeConfigSource::Cli,
2449 ));
2450 crate::runtime_env::push_cli_runtime_entry(
2451 &mut entries,
2452 "FERRUM_KV_DTYPE",
2453 cmd.kv_dtype.as_deref(),
2454 );
2455 crate::runtime_env::push_cli_runtime_usize(&mut entries, "FERRUM_KV_CAPACITY", cmd.kv_capacity);
2456 crate::runtime_env::push_cli_runtime_usize(
2457 &mut entries,
2458 "FERRUM_KV_MAX_BLOCKS",
2459 cmd.kv_max_blocks,
2460 );
2461 crate::runtime_env::push_cli_runtime_usize(
2462 &mut entries,
2463 "FERRUM_MAX_MODEL_LEN",
2464 cmd.max_model_len,
2465 );
2466 crate::runtime_env::push_cli_runtime_usize(
2467 &mut entries,
2468 "FERRUM_PAGED_MAX_SEQS",
2469 cmd.max_num_seqs,
2470 );
2471 crate::runtime_env::push_cli_runtime_usize(
2472 &mut entries,
2473 "FERRUM_MAX_BATCHED_TOKENS",
2474 cmd.max_num_batched_tokens,
2475 );
2476 crate::runtime_env::push_cli_runtime_entry(
2477 &mut entries,
2478 "FERRUM_SEQUENCE_FIT_POLICY",
2479 cmd.sequence_fit_policy
2480 .map(crate::commands::SequenceFitPolicyArg::as_runtime_value),
2481 );
2482 crate::runtime_env::push_cli_runtime_entry(
2483 &mut entries,
2484 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
2485 cmd.vnext_diagnostic_fault
2486 .map(crate::commands::VNextDiagnosticFaultArg::as_runtime_value),
2487 );
2488 crate::runtime_env::push_cli_runtime_usize(
2489 &mut entries,
2490 "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
2491 cmd.runtime_memory_budget_bytes
2492 .map(std::num::NonZeroUsize::get),
2493 );
2494 if let Some(enabled) = bool_cli_override(cmd.batched_graph, cmd.disable_batched_graph) {
2495 entries.push(RuntimeConfigEntry::new(
2496 "FERRUM_BATCHED_GRAPH",
2497 if enabled { "1" } else { "0" },
2498 RuntimeConfigSource::Cli,
2499 ));
2500 }
2501 if let Some(enabled) = bool_cli_override(cmd.reusable_execution, cmd.disable_reusable_execution)
2502 {
2503 entries.push(RuntimeConfigEntry::new(
2504 "FERRUM_REUSABLE_EXECUTION",
2505 if enabled { "1" } else { "0" },
2506 RuntimeConfigSource::Cli,
2507 ));
2508 }
2509 if let Some(enabled) = bool_cli_override(cmd.unified_graph, cmd.disable_unified_graph) {
2510 entries.push(RuntimeConfigEntry::new(
2511 "FERRUM_UNIFIED_GRAPH",
2512 if enabled { "1" } else { "0" },
2513 RuntimeConfigSource::Cli,
2514 ));
2515 }
2516 if let Some(enabled) = bool_cli_override(
2517 cmd.unified_graph_layers_only,
2518 cmd.disable_unified_graph_layers_only,
2519 ) {
2520 entries.push(RuntimeConfigEntry::new(
2521 "FERRUM_UNIFIED_GRAPH_LAYERS_ONLY",
2522 if enabled { "1" } else { "0" },
2523 RuntimeConfigSource::Cli,
2524 ));
2525 }
2526 if let Some(enabled) = bool_cli_override(
2527 cmd.unified_graph_lm_head_eager,
2528 cmd.disable_unified_graph_lm_head_eager,
2529 ) {
2530 entries.push(RuntimeConfigEntry::new(
2531 "FERRUM_UNIFIED_GRAPH_LM_HEAD_EAGER",
2532 if enabled { "1" } else { "0" },
2533 RuntimeConfigSource::Cli,
2534 ));
2535 }
2536 crate::layer_split_pipeline::push_cli_runtime_entry(
2537 &mut entries,
2538 cmd.layer_split_pipeline_mode,
2539 );
2540 if let Some(path) = &cmd.profile_jsonl {
2541 entries.push(RuntimeConfigEntry::new(
2542 "FERRUM_PROFILE_JSONL",
2543 path.to_string_lossy().to_string(),
2544 RuntimeConfigSource::Cli,
2545 ));
2546 }
2547 if let Some(path) = &cmd.scheduler_trace_jsonl {
2548 entries.push(RuntimeConfigEntry::new(
2549 "FERRUM_SCHEDULER_TRACE_JSONL",
2550 path.to_string_lossy().to_string(),
2551 RuntimeConfigSource::Cli,
2552 ));
2553 }
2554 if cmd.profile_jsonl.is_some() || cmd.scheduler_trace_jsonl.is_some() {
2555 entries.push(RuntimeConfigEntry::new(
2556 "FERRUM_PROFILE_ENTRYPOINT",
2557 "run",
2558 RuntimeConfigSource::Cli,
2559 ));
2560 }
2561 if let Some(selection) = gpu_selection {
2562 entries.extend(selection.runtime_config_entries());
2563 }
2564 entries
2565}
2566
2567fn bool_cli_override(enable: bool, disable: bool) -> Option<bool> {
2568 if enable {
2569 Some(true)
2570 } else if disable {
2571 Some(false)
2572 } else {
2573 None
2574 }
2575}
2576
2577fn materialize_run_cli_runtime_entries(entries: &[RuntimeConfigEntry]) {
2578 if entries.is_empty() {
2579 return;
2580 }
2581 crate::runtime_env::materialize_runtime_env_effective(&RuntimeConfigSnapshot::from_entries(
2582 entries.to_vec(),
2583 ));
2584}
2585
2586fn run_startup_auto_config(
2587 device: &ferrum_types::Device,
2588 typed_model_capabilities: Option<ModelCapabilities>,
2589 execution_resource_authority: ferrum_types::ExecutionResourceAuthority,
2590 model_definition: Option<&ferrum_models::ModelDefinition>,
2591 model_weight_bytes: Option<u64>,
2592 runtime_config: RuntimeConfigSnapshot,
2593) -> Result<ResolvedFerrumConfig> {
2594 let hardware = crate::commands::serve::hardware_capabilities_for_device(device);
2595 let model = typed_model_capabilities
2596 .or_else(|| model_definition.map(|definition| {
2597 crate::commands::serve::model_capabilities_from_definition_with_weight_bytes_for_hardware(
2598 definition,
2599 model_weight_bytes,
2600 &hardware,
2601 )
2602 }))
2603 .unwrap_or_else(ModelCapabilities::unknown);
2604 let workload = WorkloadProfile::serving_default();
2605 FerrumConfigBuilder::new(runtime_config)
2606 .with_model_capabilities(model)
2607 .with_hardware_capabilities(hardware)
2608 .with_workload_profile(workload)
2609 .with_execution_resource_authority(execution_resource_authority)
2610 .resolve()
2611 .map_err(|err| ferrum_types::FerrumError::config(format!("invalid auto config: {err}")))
2612}
2613
2614pub fn apply_kv_dtype_override(
2619 engine_config: &mut ferrum_types::EngineConfig,
2620 raw: Option<&str>,
2621) -> ferrum_types::Result<()> {
2622 use ferrum_types::KvCacheDtype;
2623 let Some(raw) = raw else {
2624 return Ok(());
2626 };
2627 let parsed = KvCacheDtype::parse(raw).ok_or_else(|| {
2628 ferrum_types::FerrumError::config(format!(
2629 "Unknown --kv-dtype value '{}'. Accepts: fp16, bf16, int8, fp8.",
2630 raw
2631 ))
2632 })?;
2633 match parsed {
2634 KvCacheDtype::Fp16 => {
2635 engine_config.kv_cache.dtype = KvCacheDtype::Fp16;
2636 Ok(())
2637 }
2638 KvCacheDtype::Int8 => {
2639 engine_config.kv_cache.dtype = KvCacheDtype::Int8;
2644 Ok(())
2645 }
2646 KvCacheDtype::Fp8 => Err(ferrum_types::FerrumError::unsupported(
2647 "FP8 KV cache: kernels not yet implemented. Tracked as PR D.",
2648 )),
2649 KvCacheDtype::Bf16 => Err(ferrum_types::FerrumError::unsupported(
2650 "BF16 KV cache: marker only, no backend impl ships yet.",
2651 )),
2652 }
2653}
2654
2655#[cfg(test)]
2656mod tests {
2657 use super::*;
2658 use ferrum_types::{RuntimeConfigSource, SequenceFitPolicy, TokenId};
2659
2660 fn default_params(max_tokens: usize) -> SamplingParams {
2661 SamplingParams {
2662 max_tokens,
2663 ..SamplingParams::default()
2664 }
2665 }
2666
2667 fn test_run_cmd() -> RunCommand {
2668 RunCommand {
2669 model: Some("tinyllama".to_string()),
2670 product_sources: crate::source_resolver::ProductSourceArgs::default(),
2671 system: None,
2672 max_tokens: 4096,
2673 stop: Vec::new(),
2674 no_context_shift: false,
2675 enable_thinking: false,
2676 disable_thinking: false,
2677 temperature: 0.0,
2678 backend: "auto".to_string(),
2679 gpu_devices: None,
2680 layer_split_pipeline_mode: None,
2681 prompt: None,
2682 tokenizer: None,
2683 bench_mode: false,
2684 top_k: 50,
2685 top_p: 0.95,
2686 min_p: 0.0,
2687 presence_penalty: 0.0,
2688 repeat_penalty: 1.0,
2689 repeat_last_n: 64,
2690 seed: None,
2691 gpu_memory_utilization: 0.9,
2692 runtime_memory_budget_bytes: None,
2693 max_model_len: None,
2694 max_num_seqs: None,
2695 max_num_batched_tokens: None,
2696 sequence_fit_policy: None,
2697 batched_graph: false,
2698 disable_batched_graph: false,
2699 reusable_execution: false,
2700 disable_reusable_execution: false,
2701 unified_graph: false,
2702 disable_unified_graph: false,
2703 unified_graph_layers_only: false,
2704 disable_unified_graph_layers_only: false,
2705 unified_graph_lm_head_eager: false,
2706 disable_unified_graph_lm_head_eager: false,
2707 kv_dtype: None,
2708 kv_capacity: None,
2709 kv_max_blocks: None,
2710 effective_config_json: None,
2711 decision_trace_jsonl: None,
2712 observability_vertical_slice_out: None,
2713 vnext_checkpoint: Default::default(),
2714 profile_jsonl: None,
2715 profile_detail: crate::observability_product::ProfileDetailArg::Off,
2716 vnext_diagnostic_fault: None,
2717 memory_profile_jsonl: None,
2718 scheduler_trace_jsonl: None,
2719 request_dump_dir: None,
2720 profile_sample_rate: crate::observability_product::default_profile_sample_rate(),
2721 output_format: OutputFormat::Text,
2722 }
2723 }
2724
2725 #[test]
2726 fn teacher_forced_checkpoint_is_one_shot_single_sequence_and_deterministic() {
2727 let teacher =
2728 ferrum_types::VNextTeacherForcingConfig::new(vec![TokenId::new(7), TokenId::new(11)])
2729 .unwrap();
2730 let capture = ferrum_types::VNextCheckpointCaptureConfig {
2731 output_dir: PathBuf::from("capture"),
2732 value_ids: Vec::new(),
2733 maximum_prefill_waves: 1,
2734 maximum_decode_waves: 1,
2735 capture_product_output: true,
2736 teacher_forcing: Some(teacher),
2737 };
2738 let mut cmd = test_run_cmd();
2739 cmd.prompt = Some("hello".to_owned());
2740 cmd.max_tokens = 2;
2741 cmd.max_num_seqs = Some(1);
2742 cmd.top_k = 0;
2743 cmd.top_p = 1.0;
2744 assert!(validate_teacher_forced_checkpoint_run(&cmd, Some(&capture)).is_ok());
2745
2746 cmd.prompt = None;
2747 assert!(validate_teacher_forced_checkpoint_run(&cmd, Some(&capture)).is_err());
2748 cmd.prompt = Some("hello".to_owned());
2749 cmd.max_num_seqs = Some(2);
2750 assert!(validate_teacher_forced_checkpoint_run(&cmd, Some(&capture)).is_err());
2751 cmd.max_num_seqs = Some(1);
2752 cmd.temperature = 0.5;
2753 assert!(validate_teacher_forced_checkpoint_run(&cmd, Some(&capture)).is_err());
2754 }
2755
2756 fn whitespace_budget(kv_capacity: usize) -> RunBudget {
2757 RunBudget {
2758 tokenizer: None,
2759 kv_capacity: Some(kv_capacity),
2760 prompt_token_id_mapper: None,
2761 prompt_token_counter: Some(|prompt| prompt.split_whitespace().count()),
2762 }
2763 }
2764
2765 fn mapped_token_budget(kv_capacity: usize) -> RunBudget {
2766 RunBudget {
2767 tokenizer: None,
2768 kv_capacity: Some(kv_capacity),
2769 prompt_token_id_mapper: Some(|prompt| {
2770 prompt
2771 .split_whitespace()
2772 .enumerate()
2773 .map(|(index, _)| (index + 1) as u32)
2774 .collect()
2775 }),
2776 prompt_token_counter: None,
2777 }
2778 }
2779
2780 fn default_template_options() -> ChatTemplateOptions {
2781 ChatTemplateOptions::default()
2782 }
2783
2784 #[test]
2785 fn run_effective_runtime_config_records_cli_kv_dtype() {
2786 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
2787 let mut cmd = test_run_cmd();
2788 cmd.kv_dtype = Some("int8".to_string());
2789 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
2790 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
2791 let entry = effective
2792 .entries
2793 .iter()
2794 .find(|entry| entry.key == "FERRUM_KV_DTYPE")
2795 .expect("missing kv dtype entry");
2796 assert_eq!(entry.effective_value, "int8");
2797 assert_eq!(entry.source, RuntimeConfigSource::Cli);
2798 }
2799
2800 #[test]
2801 fn run_exposes_typed_diagnostic_fault_and_records_cli_authority() {
2802 use clap::Parser;
2803
2804 #[derive(Parser)]
2805 struct TestCli {
2806 #[command(flatten)]
2807 run: RunCommand,
2808 }
2809
2810 let parsed = TestCli::parse_from([
2811 "ferrum",
2812 "Qwen/Qwen3.5-4B",
2813 "--vnext-diagnostic-fault",
2814 "prefill-resource-after-submit-once",
2815 ]);
2816 let entries = run_startup_cli_runtime_entries(&parsed.run, None);
2817 let entry = entries
2818 .iter()
2819 .find(|entry| entry.key == "FERRUM_VNEXT_DIAGNOSTIC_FAULT")
2820 .expect("diagnostic fault CLI entry");
2821
2822 assert_eq!(
2823 parsed.run.vnext_diagnostic_fault,
2824 Some(crate::commands::VNextDiagnosticFaultArg::PrefillResourceAfterSubmitOnce)
2825 );
2826 assert_eq!(entry.effective_value, "prefill-resource-after-submit-once");
2827 assert_eq!(entry.source, RuntimeConfigSource::Cli);
2828 }
2829
2830 #[test]
2831 fn run_effective_runtime_config_records_memory_budget() {
2832 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
2833 let mut cmd = test_run_cmd();
2834 cmd.runtime_memory_budget_bytes = std::num::NonZeroUsize::new(12_345);
2835
2836 let effective =
2837 run_effective_runtime_config(&snapshot, &run_startup_cli_runtime_entries(&cmd, None));
2838 let entry = effective
2839 .entries
2840 .iter()
2841 .find(|entry| entry.key == "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES")
2842 .expect("missing runtime memory budget entry");
2843
2844 assert_eq!(entry.effective_value, "12345");
2845 assert_eq!(entry.source, RuntimeConfigSource::Cli);
2846 }
2847
2848 #[test]
2849 fn run_effective_runtime_config_records_observability_paths() {
2850 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
2851 let mut cmd = test_run_cmd();
2852 cmd.profile_jsonl = Some(PathBuf::from("/tmp/run-profile.jsonl"));
2853 cmd.scheduler_trace_jsonl = Some(PathBuf::from("/tmp/run-scheduler.jsonl"));
2854
2855 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
2856 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
2857 let entry = |key: &str| {
2858 effective
2859 .entries
2860 .iter()
2861 .find(|entry| entry.key == key)
2862 .unwrap_or_else(|| panic!("missing {key} entry"))
2863 };
2864
2865 assert_eq!(
2866 entry("FERRUM_PROFILE_JSONL").effective_value,
2867 "/tmp/run-profile.jsonl"
2868 );
2869 assert_eq!(
2870 entry("FERRUM_SCHEDULER_TRACE_JSONL").effective_value,
2871 "/tmp/run-scheduler.jsonl"
2872 );
2873 assert_eq!(entry("FERRUM_PROFILE_ENTRYPOINT").effective_value, "run");
2874 assert!(entry("FERRUM_PROFILE_ENTRYPOINT")
2875 .affects
2876 .contains(&ferrum_types::RuntimeConfigEffect::Diagnostics));
2877 }
2878
2879 #[test]
2880 fn run_full_profile_detail_reaches_typed_engine_config() {
2881 let mut cmd = test_run_cmd();
2882 cmd.profile_detail = crate::observability_product::ProfileDetailArg::Full;
2883 let effective = run_effective_runtime_config(
2884 &RuntimeConfigSnapshot::from_entries(Vec::new()),
2885 &run_startup_cli_runtime_entries(&cmd, None),
2886 );
2887 let mut engine = ferrum_types::EngineConfig::default();
2888
2889 engine
2890 .apply_runtime_config_snapshot(&effective)
2891 .expect("full profile detail should apply");
2892
2893 assert_eq!(
2894 engine.runtime.profile_detail,
2895 ferrum_types::ObservabilityProfileDetail::Full
2896 );
2897 }
2898
2899 #[test]
2900 fn run_latency_profile_detail_reaches_typed_engine_config() {
2901 let mut cmd = test_run_cmd();
2902 cmd.profile_detail = crate::observability_product::ProfileDetailArg::Latency;
2903 let effective = run_effective_runtime_config(
2904 &RuntimeConfigSnapshot::from_entries(Vec::new()),
2905 &run_startup_cli_runtime_entries(&cmd, None),
2906 );
2907 let mut engine = ferrum_types::EngineConfig::default();
2908
2909 engine
2910 .apply_runtime_config_snapshot(&effective)
2911 .expect("latency profile detail should apply");
2912
2913 assert_eq!(
2914 engine.runtime.profile_detail,
2915 ferrum_types::ObservabilityProfileDetail::Latency
2916 );
2917 }
2918
2919 #[test]
2920 fn run_replay_profile_detail_reaches_typed_engine_config() {
2921 let mut cmd = test_run_cmd();
2922 cmd.profile_detail = crate::observability_product::ProfileDetailArg::Replay;
2923 let effective = run_effective_runtime_config(
2924 &RuntimeConfigSnapshot::from_entries(Vec::new()),
2925 &run_startup_cli_runtime_entries(&cmd, None),
2926 );
2927 let mut engine = ferrum_types::EngineConfig::default();
2928
2929 engine
2930 .apply_runtime_config_snapshot(&effective)
2931 .expect("replay profile detail should apply");
2932
2933 assert_eq!(
2934 engine.runtime.profile_detail,
2935 ferrum_types::ObservabilityProfileDetail::Replay
2936 );
2937 }
2938
2939 #[test]
2940 fn run_verify_profile_detail_reaches_typed_engine_config() {
2941 let mut cmd = test_run_cmd();
2942 cmd.profile_detail = crate::observability_product::ProfileDetailArg::Verify;
2943 let effective = run_effective_runtime_config(
2944 &RuntimeConfigSnapshot::from_entries(Vec::new()),
2945 &run_startup_cli_runtime_entries(&cmd, None),
2946 );
2947 let mut engine = ferrum_types::EngineConfig::default();
2948
2949 engine
2950 .apply_runtime_config_snapshot(&effective)
2951 .expect("verify profile detail should apply");
2952
2953 assert_eq!(
2954 engine.runtime.profile_detail,
2955 ferrum_types::ObservabilityProfileDetail::Verify
2956 );
2957 }
2958
2959 #[test]
2960 fn run_effective_runtime_config_records_layer_split_pipeline_mode() {
2961 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
2962 let mut cmd = test_run_cmd();
2963 cmd.layer_split_pipeline_mode =
2964 Some(crate::layer_split_pipeline::LayerSplitPipelineModeArg::Batch);
2965 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
2966 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
2967 let entry = effective
2968 .entries
2969 .iter()
2970 .find(|entry| entry.key == crate::layer_split_pipeline::LAYER_SPLIT_PIPELINE_MODE_KEY)
2971 .expect("missing layer split pipeline mode entry");
2972 assert_eq!(entry.effective_value, "batch");
2973 assert_eq!(entry.source, RuntimeConfigSource::Cli);
2974 }
2975
2976 #[test]
2977 fn run_effective_runtime_config_records_batched_graph_flag() {
2978 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
2979 let mut cmd = test_run_cmd();
2980 cmd.batched_graph = true;
2981 cmd.disable_reusable_execution = true;
2982 cmd.unified_graph = true;
2983 cmd.unified_graph_layers_only = true;
2984 cmd.unified_graph_lm_head_eager = true;
2985 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
2986 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
2987 let entry = |key: &str| {
2988 effective
2989 .entries
2990 .iter()
2991 .find(|entry| entry.key == key)
2992 .unwrap_or_else(|| panic!("missing {key} entry"))
2993 };
2994 assert_eq!(entry("FERRUM_BATCHED_GRAPH").effective_value, "1");
2995 assert_eq!(
2996 entry("FERRUM_BATCHED_GRAPH").source,
2997 RuntimeConfigSource::Cli
2998 );
2999 assert_eq!(entry("FERRUM_REUSABLE_EXECUTION").effective_value, "0");
3000 assert_eq!(
3001 entry("FERRUM_REUSABLE_EXECUTION").source,
3002 RuntimeConfigSource::Cli
3003 );
3004 assert_eq!(entry("FERRUM_UNIFIED_GRAPH").effective_value, "1");
3005 assert_eq!(
3006 entry("FERRUM_UNIFIED_GRAPH").source,
3007 RuntimeConfigSource::Cli
3008 );
3009 assert_eq!(
3010 entry("FERRUM_UNIFIED_GRAPH_LAYERS_ONLY").effective_value,
3011 "1"
3012 );
3013 assert_eq!(
3014 entry("FERRUM_UNIFIED_GRAPH_LAYERS_ONLY").source,
3015 RuntimeConfigSource::Cli
3016 );
3017 assert_eq!(
3018 entry("FERRUM_UNIFIED_GRAPH_LM_HEAD_EAGER").effective_value,
3019 "1"
3020 );
3021 assert_eq!(
3022 entry("FERRUM_UNIFIED_GRAPH_LM_HEAD_EAGER").source,
3023 RuntimeConfigSource::Cli
3024 );
3025 }
3026
3027 #[test]
3028 fn run_effective_runtime_config_records_gpu_device_selection() {
3029 let selection = crate::gpu_devices::GpuDeviceSelection {
3030 raw_cli_value: "1".to_string(),
3031 requested_gpu_devices: vec![1],
3032 selected_gpu_devices: vec![1],
3033 cuda_device_count: 2,
3034 selected_distributed_strategy: "single_gpu".to_string(),
3035 selected_layer_split_plan: None,
3036 selected_layer_split_stages: None,
3037 };
3038 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
3039 let cmd = test_run_cmd();
3040 let cli_entries = run_startup_cli_runtime_entries(&cmd, Some(&selection));
3041 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
3042 let entry = |key: &str| {
3043 effective
3044 .entries
3045 .iter()
3046 .find(|entry| entry.key == key)
3047 .unwrap_or_else(|| panic!("missing {key}"))
3048 };
3049
3050 assert_eq!(entry("FERRUM_BACKEND").effective_value, "cuda");
3051 assert_eq!(entry("FERRUM_REQUESTED_GPU_DEVICES").effective_value, "1");
3052 assert_eq!(entry("FERRUM_SELECTED_GPU_DEVICES").effective_value, "1");
3053 assert_eq!(
3054 entry("FERRUM_SELECTED_DISTRIBUTED_STRATEGY").effective_value,
3055 "single_gpu"
3056 );
3057 }
3058
3059 #[test]
3060 fn run_effective_runtime_config_records_cli_runtime_limits() {
3061 let mut cmd = test_run_cmd();
3062 cmd.kv_capacity = Some(2048);
3063 cmd.kv_max_blocks = Some(4096);
3064 cmd.max_model_len = Some(8192);
3065 cmd.max_num_seqs = Some(8);
3066 cmd.max_num_batched_tokens = Some(1024);
3067 cmd.sequence_fit_policy = Some(crate::commands::SequenceFitPolicyArg::FullInputMustFit);
3068 let snapshot = RuntimeConfigSnapshot::from_entries(Vec::new());
3069 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
3070 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
3071 let entry = |key: &str| {
3072 effective
3073 .entries
3074 .iter()
3075 .find(|entry| entry.key == key)
3076 .unwrap_or_else(|| panic!("missing {key}"))
3077 };
3078
3079 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "2048");
3080 assert_eq!(entry("FERRUM_KV_MAX_BLOCKS").effective_value, "4096");
3081 assert_eq!(entry("FERRUM_MAX_MODEL_LEN").effective_value, "8192");
3082 assert_eq!(entry("FERRUM_PAGED_MAX_SEQS").effective_value, "8");
3083 assert_eq!(entry("FERRUM_MAX_BATCHED_TOKENS").effective_value, "1024");
3084 assert_eq!(
3085 entry("FERRUM_SEQUENCE_FIT_POLICY").effective_value,
3086 "full-input-must-fit"
3087 );
3088 assert_eq!(
3089 entry("FERRUM_MAX_MODEL_LEN").source,
3090 RuntimeConfigSource::Cli
3091 );
3092 }
3093
3094 #[test]
3095 fn run_runtime_config_precedence_is_config_then_env_then_cli() {
3096 let mut config = CliConfig::default();
3097 config.runtime.sequence_fit_policy = Some(SequenceFitPolicy::FullInputMustFit);
3098
3099 let config_only =
3100 run_base_runtime_config(&config, RuntimeConfigSnapshot::from_entries(Vec::new()));
3101 let config_entry = config_only
3102 .entries
3103 .iter()
3104 .find(|entry| entry.key == "FERRUM_SEQUENCE_FIT_POLICY")
3105 .expect("config sequence fit policy is missing");
3106 assert_eq!(config_entry.effective_value, "full-input-must-fit");
3107 assert_eq!(config_entry.source, RuntimeConfigSource::ConfigFile);
3108
3109 let env_wins = run_base_runtime_config(
3110 &config,
3111 RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
3112 "FERRUM_SEQUENCE_FIT_POLICY",
3113 "immediate-only",
3114 RuntimeConfigSource::Env,
3115 )]),
3116 );
3117 let env_entry = env_wins
3118 .entries
3119 .iter()
3120 .find(|entry| entry.key == "FERRUM_SEQUENCE_FIT_POLICY")
3121 .expect("env sequence fit policy is missing");
3122 assert_eq!(env_entry.effective_value, "immediate-only");
3123 assert_eq!(env_entry.source, RuntimeConfigSource::Env);
3124
3125 let mut cmd = test_run_cmd();
3126 cmd.sequence_fit_policy = Some(crate::commands::SequenceFitPolicyArg::FullInputMustFit);
3127 let effective =
3128 run_effective_runtime_config(&env_wins, &run_startup_cli_runtime_entries(&cmd, None));
3129 let cli_entry = effective
3130 .entries
3131 .iter()
3132 .find(|entry| entry.key == "FERRUM_SEQUENCE_FIT_POLICY")
3133 .expect("CLI sequence fit policy is missing");
3134 assert_eq!(cli_entry.effective_value, "full-input-must-fit");
3135 assert_eq!(cli_entry.source, RuntimeConfigSource::Cli);
3136 }
3137
3138 #[test]
3139 fn run_product_defaults_are_typed_and_apply_to_engine_config() {
3140 let config = CliConfig::default();
3141 let effective =
3142 run_base_runtime_config(&config, RuntimeConfigSnapshot::from_entries(Vec::new()));
3143 let entry = |key: &str| {
3144 effective
3145 .entries
3146 .iter()
3147 .find(|entry| entry.key == key)
3148 .unwrap_or_else(|| panic!("missing {key}"))
3149 };
3150
3151 assert_eq!(entry("FERRUM_PAGED_MAX_SEQS").effective_value, "1");
3152 assert_eq!(
3153 entry("FERRUM_PAGED_MAX_SEQS").source,
3154 RuntimeConfigSource::Default
3155 );
3156 assert_eq!(
3157 entry("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS").effective_value,
3158 "1"
3159 );
3160 assert_eq!(
3161 entry("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS").source,
3162 RuntimeConfigSource::Default
3163 );
3164
3165 let mut engine_config = ferrum_types::EngineConfig::default();
3166 engine_config
3167 .apply_runtime_config_snapshot(&effective)
3168 .expect("run defaults should apply to engine config");
3169 assert_eq!(engine_config.scheduler.max_running_requests, 1);
3170 assert_eq!(
3171 engine_config
3172 .backend
3173 .reusable_execution_capture
3174 .exact_decode_widths,
3175 Some(vec![1])
3176 );
3177 }
3178
3179 #[test]
3180 fn run_product_config_and_env_override_entrypoint_defaults() {
3181 let mut config = CliConfig::default();
3182 config.runtime.paged_max_seqs = Some(4);
3183 config.runtime.reusable_execution_exact_decode_widths = Some(vec![1, 2, 4]);
3184
3185 let config_only =
3186 run_base_runtime_config(&config, RuntimeConfigSnapshot::from_entries(Vec::new()));
3187 let config_entry = |key: &str| {
3188 config_only
3189 .entries
3190 .iter()
3191 .find(|entry| entry.key == key)
3192 .unwrap_or_else(|| panic!("missing {key}"))
3193 };
3194 assert_eq!(config_entry("FERRUM_PAGED_MAX_SEQS").effective_value, "4");
3195 assert_eq!(
3196 config_entry("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS").effective_value,
3197 "1,2,4"
3198 );
3199 assert_eq!(
3200 config_entry("FERRUM_PAGED_MAX_SEQS").source,
3201 RuntimeConfigSource::ConfigFile
3202 );
3203
3204 let env_wins = run_base_runtime_config(
3205 &config,
3206 RuntimeConfigSnapshot::from_entries([
3207 RuntimeConfigEntry::new("FERRUM_PAGED_MAX_SEQS", "8", RuntimeConfigSource::Env),
3208 RuntimeConfigEntry::new(
3209 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS",
3210 "1,2,4,8",
3211 RuntimeConfigSource::Env,
3212 ),
3213 ]),
3214 );
3215 let env_entry = |key: &str| {
3216 env_wins
3217 .entries
3218 .iter()
3219 .find(|entry| entry.key == key)
3220 .unwrap_or_else(|| panic!("missing {key}"))
3221 };
3222 assert_eq!(env_entry("FERRUM_PAGED_MAX_SEQS").effective_value, "8");
3223 assert_eq!(
3224 env_entry("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS").effective_value,
3225 "1,2,4,8"
3226 );
3227 assert_eq!(
3228 env_entry("FERRUM_PAGED_MAX_SEQS").source,
3229 RuntimeConfigSource::Env
3230 );
3231
3232 let mut cmd = test_run_cmd();
3233 cmd.max_num_seqs = Some(16);
3234 let cli_wins =
3235 run_effective_runtime_config(&env_wins, &run_startup_cli_runtime_entries(&cmd, None));
3236 let admission = cli_wins
3237 .entries
3238 .iter()
3239 .find(|entry| entry.key == "FERRUM_PAGED_MAX_SEQS")
3240 .expect("CLI admission is missing");
3241 assert_eq!(admission.effective_value, "16");
3242 assert_eq!(admission.source, RuntimeConfigSource::Cli);
3243 }
3244
3245 #[test]
3246 fn run_effective_runtime_config_applies_recurrent_state_slots_to_engine_config() {
3247 let cmd = test_run_cmd();
3248 let snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
3249 "FERRUM_RECURRENT_STATE_MAX_SLOTS",
3250 "16",
3251 RuntimeConfigSource::ConfigFile,
3252 )]);
3253 let cli_entries = run_startup_cli_runtime_entries(&cmd, None);
3254 let effective = run_effective_runtime_config(&snapshot, &cli_entries);
3255 let mut engine_config = ferrum_types::EngineConfig::default();
3256
3257 engine_config
3258 .apply_runtime_config_snapshot(&effective)
3259 .expect("run effective runtime config should apply");
3260
3261 assert_eq!(engine_config.runtime.recurrent_state_max_slots, Some(16));
3262 }
3263
3264 #[test]
3265 fn run_startup_auto_config_renders_effective_config_schema() {
3266 let resolved = run_startup_auto_config(
3267 &ferrum_types::Device::CPU,
3268 None,
3269 ferrum_types::ExecutionResourceAuthority::LegacyEngine,
3270 None,
3271 None,
3272 RuntimeConfigSnapshot::from_entries(Vec::new()),
3273 )
3274 .expect("auto config");
3275 let doc = resolved.effective_config_document();
3276 assert_eq!(doc["schema_version"], 1);
3277 assert!(doc["entries"].is_array());
3278 assert!(doc["model_capabilities"].is_object());
3279 assert!(doc["hardware_capabilities"].is_object());
3280 assert!(doc["workload_profile"].is_object());
3281 assert_eq!(doc["workload_profile"]["target_concurrency"], 1);
3282 assert_eq!(doc["admission"]["effective_max_concurrent"], 1);
3283 assert!(doc["decisions"].is_array());
3284 }
3285
3286 #[test]
3287 fn unknown_backend_is_rejected() {
3288 let err = select_device("not-a-backend").expect_err("unknown backend must fail");
3289 assert!(
3290 err.to_string().contains("unknown backend"),
3291 "unexpected error: {err}"
3292 );
3293 }
3294
3295 #[cfg(not(all(target_os = "macos", feature = "metal")))]
3296 #[test]
3297 fn explicit_metal_backend_without_compiled_support_is_rejected() {
3298 let err = select_device("metal").expect_err("unsupported explicit Metal must fail");
3299 assert!(
3300 err.to_string()
3301 .contains("requested backend 'metal' but this ferrum binary was not built"),
3302 "unexpected error: {err}"
3303 );
3304 }
3305
3306 #[cfg(not(feature = "cuda"))]
3307 #[test]
3308 fn explicit_cuda_backend_without_compiled_support_is_rejected() {
3309 let err = select_device("cuda").expect_err("unsupported explicit CUDA must fail");
3310 assert!(
3311 err.to_string()
3312 .contains("requested backend 'cuda' but this ferrum binary was not built"),
3313 "unexpected error: {err}"
3314 );
3315 }
3316
3317 #[test]
3318 fn run_metadata_forbids_initial_thinking_close_without_open_block() {
3319 let metadata =
3320 run_request_metadata("<|im_start|>assistant\n", &ChatTemplateOptions::default());
3321 let forbidden = metadata
3322 .get(RUN_INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY)
3323 .and_then(|value| value.as_array())
3324 .expect("initial forbidden token texts");
3325 assert_eq!(
3326 forbidden,
3327 &[serde_json::Value::String(THINK_END_TAG.to_string())]
3328 );
3329 }
3330
3331 #[test]
3332 fn run_thinking_options_preserve_model_default_and_explicit_overrides() {
3333 let template = ModelChatTemplate::new(
3334 "{% if enable_thinking is defined %}{{ enable_thinking }}{% endif %}",
3335 "thinking-template",
3336 );
3337 let mut cmd = test_run_cmd();
3338
3339 assert_eq!(
3340 build_chat_template_options(&cmd, Some(&template)).enable_thinking,
3341 None
3342 );
3343
3344 cmd.enable_thinking = true;
3345 assert_eq!(
3346 build_chat_template_options(&cmd, Some(&template)).enable_thinking,
3347 Some(true)
3348 );
3349
3350 cmd.enable_thinking = false;
3351 cmd.disable_thinking = true;
3352 assert_eq!(
3353 build_chat_template_options(&cmd, Some(&template)).enable_thinking,
3354 Some(false)
3355 );
3356 }
3357
3358 #[test]
3359 fn run_metadata_forbids_initial_thinking_start_when_template_disables_thinking() {
3360 let metadata = run_request_metadata(
3361 "<|im_start|>assistant\n<think>\n\n</think>\n\n",
3362 &ChatTemplateOptions {
3363 enable_thinking: Some(false),
3364 ..Default::default()
3365 },
3366 );
3367 let forbidden = metadata
3368 .get(RUN_INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY)
3369 .and_then(|value| value.as_array())
3370 .expect("initial forbidden token texts");
3371 assert_eq!(
3372 forbidden,
3373 &[
3374 serde_json::Value::String(THINK_END_TAG.to_string()),
3375 serde_json::Value::String(THINK_START_TAG.to_string()),
3376 ]
3377 );
3378 }
3379
3380 #[test]
3381 fn run_metadata_allows_thinking_close_when_prompt_opened_block() {
3382 let metadata = run_request_metadata(
3383 "<|im_start|>assistant\n<think>\n",
3384 &ChatTemplateOptions::default(),
3385 );
3386 assert!(!metadata.contains_key(RUN_INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
3387 }
3388
3389 #[test]
3390 fn jsonl_v2_assistant_binds_reasoning_usage_and_history() {
3391 let history = vec![
3392 ("user".to_string(), "first".to_string()),
3393 (
3394 "assistant".to_string(),
3395 "<think>why</think>answer".to_string(),
3396 ),
3397 ];
3398 let usage = TokenUsage::new(7, 3);
3399 let record = jsonl_assistant_record(
3400 "session-1",
3401 2,
3402 "request-1",
3403 1,
3404 "answer",
3405 Some("why"),
3406 &history,
3407 Some(FinishReason::EOS),
3408 Some(&usage),
3409 3,
3410 2,
3411 "<think>why</think>answer",
3412 12.5,
3413 );
3414 assert_eq!(record["schema_version"], RUN_JSONL_SCHEMA_VERSION);
3415 assert_eq!(record["session_id"], "session-1");
3416 assert_eq!(record["history_epoch"], 2);
3417 assert_eq!(record["request_id"], "request-1");
3418 assert_eq!(record["content"], "answer");
3419 assert_eq!(record["reasoning"], "why");
3420 assert_eq!(record["usage"]["prompt_tokens"], 7);
3421 assert_eq!(record["usage"]["completion_tokens"], 3);
3422 assert_eq!(record["usage"]["total_tokens"], 10);
3423 assert_eq!(record["history_before"]["message_count"], 2);
3424 assert_eq!(record["history_before"]["turn_count"], 1);
3425 assert_eq!(
3426 record["raw_text_sha256"],
3427 sha256_text("<think>why</think>answer")
3428 );
3429 }
3430
3431 #[test]
3432 fn history_evidence_changes_when_reasoning_history_changes() {
3433 let first = vec![("assistant".to_string(), "answer".to_string())];
3434 let second = vec![(
3435 "assistant".to_string(),
3436 "<think>why</think>answer".to_string(),
3437 )];
3438 assert_ne!(
3439 history_evidence(&first)["sha256"],
3440 history_evidence(&second)["sha256"]
3441 );
3442 }
3443
3444 #[test]
3445 fn jsonl_v2_delta_preserves_utf8_bytes_and_request_binding() {
3446 let record =
3447 jsonl_assistant_delta_record("session-1", 3, "request-1", 4, 2, "🙂", Some(9271));
3448 assert_eq!(record["event"], "assistant_delta");
3449 assert_eq!(record["session_id"], "session-1");
3450 assert_eq!(record["history_epoch"], 3);
3451 assert_eq!(record["request_id"], "request-1");
3452 assert_eq!(record["turn"], 4);
3453 assert_eq!(record["index"], 2);
3454 assert_eq!(record["raw_text_delta"], "🙂");
3455 assert_eq!(record["utf8_bytes"], 4);
3456 assert_eq!(record["token_id"], 9271);
3457 }
3458
3459 #[test]
3460 fn cli_display_preserves_thinking_markers() {
3461 assert_eq!(
3462 display_response_text("<think>\nreasoning\n</think>\n\n最终答案"),
3463 "<think>\nreasoning\n</think>\n\n最终答案"
3464 );
3465 }
3466
3467 #[test]
3468 fn cli_display_preserves_orphan_think_close() {
3469 assert_eq!(
3470 display_response_text("</think>\n\n你好!很高兴见到你。"),
3471 "</think>\n\n你好!很高兴见到你。"
3472 );
3473 }
3474
3475 #[test]
3476 fn default_run_temperature_is_greedy() {
3477 let cmd = test_run_cmd();
3478 assert_eq!(build_sampling_params(&cmd).temperature, 0.0);
3479 assert_eq!(build_sampling_params(&cmd).max_tokens, 4096);
3480 }
3481
3482 #[test]
3483 fn run_propagates_min_p_and_presence_penalty() {
3484 use clap::Parser;
3485
3486 #[derive(Parser)]
3487 struct TestCli {
3488 #[command(flatten)]
3489 run: RunCommand,
3490 }
3491
3492 let parsed = TestCli::parse_from([
3493 "ferrum",
3494 "qwen3.5",
3495 "--temperature",
3496 "1.0",
3497 "--min-p",
3498 "0.05",
3499 "--presence-penalty",
3500 "1.5",
3501 ]);
3502 let params = build_sampling_params(&parsed.run);
3503 assert_eq!(params.min_p, Some(0.05));
3504 assert_eq!(params.presence_penalty, 1.5);
3505 params
3506 .validate()
3507 .expect("official sampling controls must validate");
3508
3509 let disabled = TestCli::parse_from(["ferrum", "qwen3.5", "--min-p", "0.0"]);
3510 assert_eq!(build_sampling_params(&disabled.run).min_p, None);
3511 }
3512
3513 #[test]
3514 fn chat_default_applies_repetition_penalty() {
3515 use clap::Parser;
3522 #[derive(Parser)]
3523 struct TestCli {
3524 #[command(flatten)]
3525 run: RunCommand,
3526 }
3527 let parsed = TestCli::parse_from(["ferrum", "qwen3:0.6b"]);
3528 assert!(
3529 parsed.run.repeat_penalty > 1.0,
3530 "chat default repeat_penalty must discourage repeats, got {}",
3531 parsed.run.repeat_penalty
3532 );
3533 assert!(
3534 build_sampling_params(&parsed.run).repetition_penalty > 1.0,
3535 "build_sampling_params must propagate the default penalty"
3536 );
3537 }
3538
3539 #[test]
3540 fn run_rejects_removed_qwen35_flag() {
3541 use clap::Parser;
3542
3543 #[derive(Parser)]
3544 struct TestCli {
3545 #[command(flatten)]
3546 run: RunCommand,
3547 }
3548
3549 let error = match TestCli::try_parse_from(["ferrum", "qwen3.5", "--qwen35-reference"]) {
3550 Ok(_) => panic!("product CLI exposed the legacy Qwen3.5 reference adapter"),
3551 Err(error) => error,
3552 };
3553
3554 assert!(error.to_string().contains("--qwen35-reference"));
3555 }
3556
3557 #[test]
3558 fn run_sampling_params_include_cli_stop_sequences() {
3559 let mut cmd = test_run_cmd();
3560 cmd.stop = vec!["\n".to_string(), String::new(), "END".to_string()];
3561 let params = build_sampling_params(&cmd);
3562 assert!(params.stop_sequences.contains(&"\n".to_string()));
3563 assert!(params.stop_sequences.contains(&"END".to_string()));
3564 assert!(!params.stop_sequences.contains(&String::new()));
3565 }
3566
3567 #[test]
3568 fn response_completion_contract_is_set_on_run_prompt_plan() {
3569 let cmd = test_run_cmd();
3570 let budget = whitespace_budget(8192);
3571 let template = ModelChatTemplate::new(
3572 "{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
3573 "thinking-test-template",
3574 );
3575 let plan = build_run_prompt_plan(
3576 &[],
3577 "demo",
3578 None,
3579 "tinyllama",
3580 Some(&template),
3581 &ChatTemplateOptions::default(),
3582 &cmd,
3583 &budget,
3584 )
3585 .unwrap();
3586
3587 assert!(has_unclosed_thinking_block(&plan.prompt));
3588 assert_eq!(
3589 plan.sampling_params.response_completion_boundary,
3590 ResponseCompletionBoundary::AfterDelimiterAndPayload {
3591 delimiter: THINK_END_TAG.to_string(),
3592 alternate_envelope: None,
3593 }
3594 );
3595 }
3596
3597 #[test]
3598 fn cpu_run_skips_gpu_chat_autosize_defaults() {
3599 assert!(run_autosize_for_device(&ferrum_types::Device::CPU, 0.9).is_none());
3600 }
3601
3602 #[cfg(any(all(target_os = "macos", feature = "metal"), feature = "cuda"))]
3603 #[test]
3604 fn accelerator_run_keeps_chat_autosize_defaults() {
3605 #[cfg(all(target_os = "macos", feature = "metal"))]
3606 let device = ferrum_types::Device::Metal;
3607 #[cfg(all(feature = "cuda", not(all(target_os = "macos", feature = "metal"))))]
3608 let device = ferrum_types::Device::CUDA(0);
3609
3610 let autosize = run_autosize_for_device(&device, 0.75);
3611 assert_eq!(
3612 autosize,
3613 Some((crate::gpu_mem_autosize::AutoSizeProfile::Chat, 0.75))
3614 );
3615 }
3616
3617 #[test]
3618 fn context_shift_clamps_output_to_remaining_kv_budget() {
3619 let cmd = test_run_cmd();
3620 let budget = whitespace_budget(64);
3621 let options = default_template_options();
3622 let plan = build_run_prompt_plan(
3623 &[],
3624 "demo",
3625 None,
3626 "tinyllama",
3627 None,
3628 &options,
3629 &cmd,
3630 &budget,
3631 )
3632 .unwrap();
3633
3634 let prompt_tokens = plan.prompt_tokens.unwrap();
3635 assert!(prompt_tokens < 64);
3636 assert_eq!(plan.max_tokens_clamped_from, Some(4096));
3637 assert_eq!(plan.sampling_params.max_tokens, 64 - prompt_tokens);
3638 }
3639
3640 #[test]
3641 fn run_prompt_plan_retains_prompt_token_ids_for_observability() {
3642 let cmd = test_run_cmd();
3643 let budget = mapped_token_budget(64);
3644 let options = default_template_options();
3645 let plan = build_run_prompt_plan(
3646 &[],
3647 "demo prompt",
3648 None,
3649 "tinyllama",
3650 None,
3651 &options,
3652 &cmd,
3653 &budget,
3654 )
3655 .unwrap();
3656
3657 let token_ids = plan
3658 .prompt_token_ids
3659 .as_ref()
3660 .expect("prompt token ids should be retained");
3661 assert_eq!(plan.prompt_tokens, Some(token_ids.len()));
3662 assert!(!token_ids.is_empty());
3663 }
3664
3665 #[test]
3666 fn context_shift_drops_oldest_history_until_prompt_fits() {
3667 let cmd = test_run_cmd();
3668 let budget = whitespace_budget(64);
3669 let long = std::iter::repeat_n("old", 80).collect::<Vec<_>>().join(" ");
3670 let history = vec![
3671 ("user".to_string(), long.clone()),
3672 ("assistant".to_string(), long),
3673 ];
3674 let options = default_template_options();
3675 let plan = build_run_prompt_plan(
3676 &history,
3677 "demo",
3678 None,
3679 "tinyllama",
3680 None,
3681 &options,
3682 &cmd,
3683 &budget,
3684 )
3685 .unwrap();
3686
3687 assert_eq!(plan.dropped_history_messages, 2);
3688 assert_eq!(plan.dropped_history_turns, 1);
3689 assert!(plan.prompt_tokens.unwrap() < 64);
3690 }
3691
3692 #[test]
3693 fn context_shift_clamps_output_before_dropping_history() {
3694 let mut cmd = test_run_cmd();
3695 cmd.max_tokens = 1024;
3696 let budget = whitespace_budget(64);
3697 let history = vec![
3698 (
3699 "user".to_string(),
3700 "Remember the identifier G00-c03-001-OK.".to_string(),
3701 ),
3702 ("assistant".to_string(), "ACKNOWLEDGED".to_string()),
3703 ];
3704 let options = default_template_options();
3705 let plan = build_run_prompt_plan(
3706 &history,
3707 "What identifier did I ask you to remember?",
3708 None,
3709 "tinyllama",
3710 None,
3711 &options,
3712 &cmd,
3713 &budget,
3714 )
3715 .unwrap();
3716
3717 let prompt_tokens = plan.prompt_tokens.unwrap();
3718 assert!(prompt_tokens < 64);
3719 assert!(plan.prompt.contains("G00-c03-001-OK"));
3720 assert_eq!(plan.dropped_history_messages, 0);
3721 assert_eq!(plan.dropped_history_turns, 0);
3722 assert_eq!(plan.max_tokens_clamped_from, Some(1024));
3723 assert_eq!(plan.sampling_params.max_tokens, 64 - prompt_tokens);
3724 }
3725
3726 #[test]
3727 fn kv_budget_accepts_request_inside_capacity() {
3728 assert!(fits_kv_budget(&default_params(512), Some(64), Some(2048)));
3729 }
3730
3731 #[test]
3732 fn kv_budget_rejects_output_past_capacity() {
3733 assert!(!fits_kv_budget(&default_params(2048), Some(64), Some(2048)));
3734 }
3735
3736 #[test]
3737 fn kv_budget_rejects_prompt_at_capacity() {
3738 assert!(!fits_kv_budget(&default_params(1), Some(2048), Some(2048)));
3739 }
3740
3741 #[test]
3742 fn sibling_repo_strips_gguf_suffix_by_default() {
3743 assert_eq!(
3744 tokenizer_sibling_repo("Qwen/Qwen3-0.6B-GGUF").as_deref(),
3745 Some("Qwen/Qwen3-0.6B")
3746 );
3747 assert_eq!(tokenizer_sibling_repo("Qwen/Qwen3-0.6B"), None);
3748 }
3749
3750 #[test]
3751 fn sibling_repo_explicit_mappings_beat_strip_convention() {
3752 assert_eq!(
3755 tokenizer_sibling_repo("bartowski/Qwen2.5-Coder-32B-Instruct-GGUF").as_deref(),
3756 Some("Qwen/Qwen2.5-Coder-32B-Instruct")
3757 );
3758 assert_eq!(
3760 tokenizer_sibling_repo("bartowski/mistralai_Mistral-Small-3.2-24B-Instruct-2506-GGUF")
3761 .as_deref(),
3762 Some("unsloth/Mistral-Small-3.2-24B-Instruct-2506")
3763 );
3764 assert_eq!(
3766 tokenizer_sibling_repo("bartowski/Meta-Llama-3.1-8B-Instruct-GGUF").as_deref(),
3767 Some("unsloth/Meta-Llama-3.1-8B-Instruct")
3768 );
3769 }
3770}