roboticus_cli/cli/admin/
models.rs1use super::*;
2
3pub async fn cmd_models_list(base_url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
6 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
7 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
8 let resp = super::http_client()?
9 .get(format!("{base_url}/api/config"))
10 .send()
11 .await?;
12 let config: serde_json::Value = resp.json().await?;
13 if json {
14 println!("{}", serde_json::to_string_pretty(&config)?);
15 return Ok(());
16 }
17
18 println!("\n {BOLD}Configured Models{RESET}\n");
19
20 let primary = config
21 .pointer("/models/primary")
22 .and_then(|v| v.as_str())
23 .unwrap_or("not set");
24 println!(" {:<12} {}", format!("{GREEN}primary{RESET}"), primary);
25
26 if let Some(fallbacks) = config
27 .pointer("/models/fallbacks")
28 .and_then(|v| v.as_array())
29 {
30 for (i, fb) in fallbacks.iter().enumerate() {
31 let name = fb.as_str().unwrap_or("?");
32 println!(
33 " {:<12} {}",
34 format!("{YELLOW}fallback {}{RESET}", i + 1),
35 name
36 );
37 }
38 }
39
40 let mode = config
41 .pointer("/models/routing/mode")
42 .and_then(|v| v.as_str())
43 .unwrap_or("rule");
44 let threshold = config
45 .pointer("/models/routing/confidence_threshold")
46 .and_then(|v| v.as_f64())
47 .unwrap_or(0.9);
48 let local_first = config
49 .pointer("/models/routing/local_first")
50 .and_then(|v| v.as_bool())
51 .unwrap_or(true);
52
53 println!();
54 println!(
55 " {DIM}Routing: mode={mode}, threshold={threshold}, local_first={local_first}{RESET}"
56 );
57 println!();
58 Ok(())
59}
60
61pub async fn cmd_models_scan(
62 base_url: &str,
63 provider: Option<&str>,
64) -> Result<(), Box<dyn std::error::Error>> {
65 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
66 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
67 println!("\n {BOLD}Scanning for available models...{RESET}\n");
68
69 let resp = super::http_client()?
70 .get(format!("{base_url}/api/config"))
71 .send()
72 .await?;
73 let config: serde_json::Value = resp.json().await?;
74
75 let providers = config
76 .get("providers")
77 .and_then(|v| v.as_object())
78 .cloned()
79 .unwrap_or_default();
80
81 if providers.is_empty() {
82 println!(" No providers configured.");
83 println!();
84 return Ok(());
85 }
86
87 let client = reqwest::Client::builder()
88 .timeout(std::time::Duration::from_secs(10))
89 .build()?;
90
91 for (name, prov_config) in &providers {
92 if let Some(filter) = provider
93 && name != filter
94 {
95 continue;
96 }
97
98 let url = prov_config
99 .get("url")
100 .and_then(|v| v.as_str())
101 .unwrap_or("");
102
103 if url.is_empty() {
104 println!(" {YELLOW}{name}{RESET}: no URL configured");
105 continue;
106 }
107
108 let name_l = name.to_lowercase();
109 let url_l = url.to_lowercase();
110 let ollama_like = name_l.contains("ollama") || url_l.contains("11434");
111 let models_url = if ollama_like {
112 format!("{url}/api/tags")
113 } else {
114 format!("{url}/v1/models")
115 };
116
117 print!(" {CYAN}{name}{RESET} ({url}): ");
118
119 match client.get(&models_url).send().await {
120 Ok(resp) if resp.status().is_success() => {
121 let body: serde_json::Value = resp.json().await.unwrap_or_default();
122 let models: Vec<String> =
123 if let Some(arr) = body.get("models").and_then(|v| v.as_array()) {
124 arr.iter()
125 .filter_map(|m| {
126 m.get("name")
127 .or_else(|| m.get("model"))
128 .and_then(|v| v.as_str())
129 })
130 .map(String::from)
131 .collect()
132 } else if let Some(arr) = body.get("data").and_then(|v| v.as_array()) {
133 arr.iter()
134 .filter_map(|m| m.get("id").and_then(|v| v.as_str()))
135 .map(String::from)
136 .collect()
137 } else {
138 vec![]
139 };
140
141 if models.is_empty() {
142 println!("no models found");
143 } else {
144 println!("{} model(s)", models.len());
145 for model in &models {
146 println!(" - {model}");
147 }
148 }
149 }
150 Ok(resp) => {
151 println!("{RED}error: {}{RESET}", resp.status());
152 }
153 Err(e) => {
154 println!("{RED}unreachable: {e}{RESET}");
155 }
156 }
157 }
158
159 println!();
160 Ok(())
161}