Skip to main content

llm_kernel/discovery/
ollama.rs

1use 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
15/// Fetch available model names from an Ollama instance.
16pub 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        // The function appends /api/tags — verify no double-slash for trailing-slash inputs.
61        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}