ferrum_cli/commands/
doctor.rs1use crate::config::CliConfig;
4use clap::Args;
5use ferrum_types::Result;
6use std::process::Command;
7
8#[derive(Args)]
9pub struct DoctorCommand {
10 #[arg(value_name = "MODEL")]
12 pub model: Option<String>,
13}
14
15pub async fn execute(cmd: DoctorCommand, config: CliConfig) -> Result<()> {
16 println!("Ferrum {}", env!("CARGO_PKG_VERSION"));
17 println!(
18 "Platform: {} {}",
19 std::env::consts::OS,
20 std::env::consts::ARCH
21 );
22
23 let accelerators = compiled_accelerators();
24 if accelerators.is_empty() {
25 println!("Compiled acceleration: none (CPU/default build)");
26 } else {
27 println!("Compiled acceleration: {}", accelerators.join(", "));
28 }
29
30 if cfg!(feature = "cuda") {
31 match cuda_devices() {
32 Some(devices) => println!("CUDA devices: {devices}"),
33 None => println!(
34 "CUDA devices: not visible; check the NVIDIA driver and `nvidia-smi` before loading a model"
35 ),
36 }
37 }
38
39 let cache = crate::source_resolver::hf_cache_dir(&config);
40 println!("Model cache: {}", cache.display());
41 println!(
42 "Cache status: {}",
43 if cache.exists() {
44 "present"
45 } else {
46 "not created yet"
47 }
48 );
49
50 println!();
51 if let Some(model) = cmd.model.as_deref() {
52 let (source, format) = describe_model(model);
53 println!("Requested model: {model}");
54 println!("Resolved source: {source}");
55 println!("Expected format: {format}");
56 println!("No model was downloaded and no inference engine was started.");
57 println!();
58 println!("Next:");
59 println!(" ferrum run {model}");
60 println!(" ferrum serve --model {model} --served-model-name ferrum --port 8000");
61 } else {
62 println!("Recommended first model:");
63 if cfg!(feature = "metal") {
64 println!(
65 " Metal: ferrum run {}",
66 crate::source_resolver::METAL_FIRST_SUCCESS_MODEL
67 );
68 }
69 if cfg!(feature = "cuda") {
70 println!(
71 " CUDA: ferrum run {}",
72 crate::source_resolver::CUDA_FIRST_SUCCESS_MODEL
73 );
74 }
75 if accelerators.is_empty() {
76 println!(" Install a Metal or CUDA build for the v0.8 accelerator paths.");
77 }
78 println!("Pass a model to inspect its source without downloading it.");
79 }
80
81 Ok(())
82}
83
84fn compiled_accelerators() -> Vec<&'static str> {
85 let mut accelerators = Vec::new();
86 if cfg!(feature = "metal") {
87 accelerators.push("metal");
88 }
89 if cfg!(feature = "cuda") {
90 accelerators.push("cuda");
91 }
92 accelerators
93}
94
95fn cuda_devices() -> Option<String> {
96 let output = Command::new("nvidia-smi")
97 .args([
98 "--query-gpu=name,memory.total",
99 "--format=csv,noheader,nounits",
100 ])
101 .output()
102 .ok()?;
103 if !output.status.success() {
104 return None;
105 }
106 let devices = String::from_utf8_lossy(&output.stdout).trim().to_string();
107 (!devices.is_empty()).then_some(devices)
108}
109
110fn describe_model(model: &str) -> (String, &'static str) {
111 if let Some((repo, filename)) = crate::source_resolver::resolve_gguf_alias(model) {
112 return (format!("{repo} / {filename}"), "GGUF");
113 }
114
115 let source = crate::source_resolver::resolve_model_alias(model);
116 let format = if model.to_ascii_lowercase().ends_with(".gguf") {
117 "GGUF"
118 } else {
119 "repository weights (resolved at startup)"
120 };
121 (source, format)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn doctor_reuses_curated_gguf_resolution() {
130 let (source, format) = describe_model("qwen3.5:4b-q4_k_m");
131 assert_eq!(format, "GGUF");
132 assert!(source.contains("unsloth/Qwen3.5-4B-GGUF"));
133 assert!(source.contains("Qwen3.5-4B-Q4_K_M.gguf"));
134 }
135
136 #[test]
137 fn doctor_reuses_hf_alias_resolution() {
138 let (source, format) = describe_model("qwen3.5:4b");
139 assert_eq!(source, "Qwen/Qwen3.5-4B");
140 assert_eq!(format, "repository weights (resolved at startup)");
141 }
142}