llm_kernel/discovery/
ollama.rs1use serde::Deserialize;
2
3use crate::error::{KernelError, Result};
4
5#[derive(Debug, Clone, Deserialize)]
6struct OllamaTag {
7 name: String,
8}
9
10#[derive(Debug, Clone, Deserialize)]
11struct OllamaResponse {
12 models: Vec<OllamaTag>,
13}
14
15pub fn fetch_ollama_models(base_url: &str) -> Result<Vec<String>> {
17 let url = format!("{}/api/tags", base_url.trim_end_matches('/'));
18 let config = ureq::config::Config::builder()
19 .timeout_global(Some(std::time::Duration::from_secs(2)))
20 .build();
21 let agent = ureq::Agent::new_with_config(config);
22 let mut resp = agent.get(&url).call().map_err(KernelError::discovery)?;
23
24 let payload: OllamaResponse = resp
25 .body_mut()
26 .read_json()
27 .map_err(KernelError::discovery)?;
28 Ok(payload.models.into_iter().map(|m| m.name).collect())
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn parse_ollama_response_multiple_models() {
37 let raw = r#"{"models":[{"name":"llama3.2:latest"},{"name":"mistral:7b"},{"name":"phi4:latest"}]}"#;
38 let resp: OllamaResponse = serde_json::from_str(raw).unwrap();
39 assert_eq!(resp.models.len(), 3);
40 assert_eq!(resp.models[0].name, "llama3.2:latest");
41 assert_eq!(resp.models[2].name, "phi4:latest");
42 }
43
44 #[test]
45 fn parse_ollama_response_empty() {
46 let raw = r#"{"models":[]}"#;
47 let resp: OllamaResponse = serde_json::from_str(raw).unwrap();
48 assert!(resp.models.is_empty());
49 }
50
51 #[test]
52 fn parse_ollama_tag_preserves_name() {
53 let raw = r#"{"name":"deepseek-r1:8b"}"#;
54 let tag: OllamaTag = serde_json::from_str(raw).unwrap();
55 assert_eq!(tag.name, "deepseek-r1:8b");
56 }
57
58 #[test]
59 fn url_construction_trims_trailing_slash() {
60 let base = "http://localhost:11434/";
62 let expected = "http://localhost:11434/api/tags";
63 let url = format!("{}/api/tags", base.trim_end_matches('/'));
64 assert_eq!(url, expected);
65 }
66}