Skip to main content

roboticus_cli/cli/admin/
models.rs

1use super::*;
2
3// ── Models ───────────────────────────────────────────────────
4
5pub 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        let scan_result =
118            super::spin_while(&format!("Probing {name}"), client.get(&models_url).send()).await;
119
120        print!("  {CYAN}{name}{RESET} ({url}): ");
121        match scan_result {
122            Ok(resp) if resp.status().is_success() => {
123                let body: serde_json::Value = resp.json().await.unwrap_or_default();
124                let models: Vec<String> =
125                    if let Some(arr) = body.get("models").and_then(|v| v.as_array()) {
126                        arr.iter()
127                            .filter_map(|m| {
128                                m.get("name")
129                                    .or_else(|| m.get("model"))
130                                    .and_then(|v| v.as_str())
131                            })
132                            .map(String::from)
133                            .collect()
134                    } else if let Some(arr) = body.get("data").and_then(|v| v.as_array()) {
135                        arr.iter()
136                            .filter_map(|m| m.get("id").and_then(|v| v.as_str()))
137                            .map(String::from)
138                            .collect()
139                    } else {
140                        vec![]
141                    };
142
143                if models.is_empty() {
144                    println!("no models found");
145                } else {
146                    println!("{} model(s)", models.len());
147                    for model in &models {
148                        println!("    - {model}");
149                    }
150                }
151            }
152            Ok(resp) => {
153                println!("{RED}error: {}{RESET}", resp.status());
154            }
155            Err(e) => {
156                println!("{RED}unreachable: {e}{RESET}");
157            }
158        }
159    }
160
161    println!();
162    Ok(())
163}