1use reqwest::Client;
2use serde::Deserialize;
3use std::time::Duration;
4
5#[derive(Debug, Deserialize)]
9struct OllamaVersion {
10 version: String,
11}
12
13#[derive(Debug, Deserialize)]
14struct OllamaTags {
15 models: Vec<OllamaModel>,
16}
17
18#[derive(Debug, Deserialize)]
19struct OllamaModel {
20 name: String,
21}
22
23pub struct OllamaHarness {
24 client: Client,
25 base_url: String,
26}
27
28impl OllamaHarness {
29 pub fn new(url: &str) -> Self {
30 Self {
31 client: Client::builder()
32 .timeout(Duration::from_secs(2))
33 .build()
34 .unwrap_or_default(),
35 base_url: url.trim_end_matches('/').to_string(),
36 }
37 }
38
39 pub async fn is_reachable(&self) -> bool {
41 self.client
42 .get(format!("{}/api/tags", self.base_url))
43 .send()
44 .await
45 .is_ok()
46 }
47
48 pub async fn verify_version(&self) -> Result<String, String> {
50 let resp = self
51 .client
52 .get(format!("{}/api/version", self.base_url))
53 .send()
54 .await
55 .map_err(|e| format!("Ollama unreachable: {}", e))?;
56
57 let ver: OllamaVersion = resp
58 .json()
59 .await
60 .map_err(|e| format!("Failed to parse Ollama version: {}", e))?;
61
62 Ok(ver.version)
64 }
65
66 pub async fn has_model(&self, name: &str) -> Result<bool, String> {
68 let resp = self
69 .client
70 .get(format!("{}/api/tags", self.base_url))
71 .send()
72 .await
73 .map_err(|e| format!("Ollama unreachable: {}", e))?;
74
75 let tags: OllamaTags = resp
76 .json()
77 .await
78 .map_err(|e| format!("Failed to parse Ollama models: {}", e))?;
79
80 let search_name = if !name.contains(':') {
82 format!("{}:latest", name)
83 } else {
84 name.to_string()
85 };
86
87 Ok(tags
88 .models
89 .iter()
90 .any(|m| m.name == name || m.name == search_name))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_ollama_url_cleanup() {
100 let harness = OllamaHarness::new("http://localhost:11434/");
101 assert_eq!(harness.base_url, "http://localhost:11434");
102 }
103}