1use anyhow::Result;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AtomicNote {
9 pub header_tags: Vec<String>,
10 pub body_text: String,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct OllamaModel {
15 pub name: String,
16 pub size: String,
17 pub modified: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct OpenRouterModel {
22 pub id: String,
23 pub name: String,
24 pub description: String,
25 pub pricing: ModelPricing,
26 pub context_length: u32,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ModelPricing {
31 pub prompt: String,
32 pub completion: String,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36struct OllamaListResponse {
37 models: Vec<OllamaModelRaw>,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
41struct OllamaModelRaw {
42 name: String,
43 size: i64,
44 modified_at: String,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48struct OpenRouterListResponse {
49 data: Vec<OpenRouterModelRaw>,
50}
51
52#[derive(Debug, Serialize, Deserialize)]
53struct OpenRouterModelRaw {
54 id: String,
55 name: String,
56 description: Option<String>,
57 pricing: ModelPricingRaw,
58 context_length: u32,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62struct ModelPricingRaw {
63 prompt: String,
64 completion: String,
65}
66
67pub struct ModelValidator {
68 client: Client,
69}
70
71impl ModelValidator {
72 pub fn new() -> Self {
73 let client = Client::builder()
74 .timeout(Duration::from_secs(5))
75 .build()
76 .unwrap_or_default();
77
78 Self { client }
79 }
80
81 pub async fn fetch_ollama_models(&self, endpoint: &str) -> Result<Vec<OllamaModel>> {
82 let url = if endpoint.starts_with("http") {
83 format!("{}/api/tags", endpoint)
84 } else {
85 format!("http://{}/api/tags", endpoint)
86 };
87
88 let response = self.client.get(&url).send().await?;
89
90 if !response.status().is_success() {
91 return Err(anyhow::anyhow!("Ollama endpoint not accessible"));
92 }
93
94 let ollama_response: OllamaListResponse = response.json().await?;
95
96 let models = ollama_response
97 .models
98 .into_iter()
99 .map(|raw| OllamaModel {
100 name: raw.name,
101 size: format_size(raw.size),
102 modified: format_relative_time(&raw.modified_at),
103 })
104 .collect();
105
106 Ok(models)
107 }
108
109 pub async fn fetch_openrouter_models(&self, api_key: &str) -> Result<Vec<OpenRouterModel>> {
110 let url = "https://openrouter.ai/api/v1/models";
111
112 let response = self
113 .client
114 .get(url)
115 .header("Authorization", format!("Bearer {}", api_key))
116 .send()
117 .await?;
118
119 if !response.status().is_success() {
120 return Err(anyhow::anyhow!(
121 "OpenRouter API not accessible or invalid API key"
122 ));
123 }
124
125 let openrouter_response: OpenRouterListResponse = response.json().await?;
126
127 let mut models: Vec<OpenRouterModel> = openrouter_response
128 .data
129 .into_iter()
130 .map(|raw| OpenRouterModel {
131 id: raw.id,
132 name: raw.name,
133 description: raw
134 .description
135 .unwrap_or_else(|| "No description available".to_string()),
136 pricing: ModelPricing {
137 prompt: raw.pricing.prompt,
138 completion: raw.pricing.completion,
139 },
140 context_length: raw.context_length,
141 })
142 .collect();
143
144 models.sort_by(|a, b| {
146 let a_is_free = a.pricing.prompt == "0" && a.pricing.completion == "0";
147 let b_is_free = b.pricing.prompt == "0" && b.pricing.completion == "0";
148
149 match (a_is_free, b_is_free) {
150 (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => a.name.cmp(&b.name), }
154 });
155
156 Ok(models)
157 }
158
159 pub async fn validate_local_endpoint(&self, endpoint: &str, model: &str) -> Result<()> {
160 let url = if endpoint.starts_with("http") {
161 format!("{}/api/tags", endpoint)
162 } else {
163 format!("http://{}/api/tags", endpoint)
164 };
165
166 let response = self.client.get(&url).send().await?;
167
168 if !response.status().is_success() {
169 return Err(anyhow::anyhow!("Local endpoint not accessible"));
170 }
171
172 let models: Value = response.json().await?;
173
174 if let Some(models_array) = models.get("models").and_then(|m| m.as_array()) {
175 let model_exists = models_array.iter().any(|m| {
176 m.get("name")
177 .and_then(|name| name.as_str())
178 .map(|name| name == model)
179 .unwrap_or(false)
180 });
181
182 if model_exists {
183 Ok(())
184 } else {
185 Err(anyhow::anyhow!(
186 "Model '{}' not found on local endpoint",
187 model
188 ))
189 }
190 } else {
191 Err(anyhow::anyhow!(
192 "Invalid response format from local endpoint"
193 ))
194 }
195 }
196
197 pub async fn validate_cloud_endpoint(&self, api_key: &str, model: &str) -> Result<()> {
198 let url = "https://openrouter.ai/api/v1/models";
199
200 let response = self
201 .client
202 .get(url)
203 .header("Authorization", format!("Bearer {}", api_key))
204 .send()
205 .await?;
206
207 if !response.status().is_success() {
208 return Err(anyhow::anyhow!(
209 "Cloud API key invalid or endpoint not accessible"
210 ));
211 }
212
213 let models: Value = response.json().await?;
214
215 if let Some(models_array) = models.get("data").and_then(|m| m.as_array()) {
216 let model_exists = models_array.iter().any(|m| {
217 m.get("id")
218 .and_then(|id| id.as_str())
219 .map(|id| id == model)
220 .unwrap_or(false)
221 });
222
223 if model_exists {
224 Ok(())
225 } else {
226 Err(anyhow::anyhow!("Model '{}' not found in OpenRouter", model))
227 }
228 } else {
229 Err(anyhow::anyhow!("Invalid response format from OpenRouter"))
230 }
231 }
232
233 pub async fn test_local_generation(&self, endpoint: &str, model: &str) -> Result<()> {
234 let url = if endpoint.starts_with("http") {
235 format!("{}/api/generate", endpoint)
236 } else {
237 format!("http://{}/api/generate", endpoint)
238 };
239
240 let payload = serde_json::json!({
241 "model": model,
242 "prompt": "Hello",
243 "stream": false,
244 "options": {
245 "num_predict": 1
246 }
247 });
248
249 let response = self.client.post(&url).json(&payload).send().await?;
250
251 if response.status().is_success() {
252 Ok(())
253 } else {
254 Err(anyhow::anyhow!(
255 "Failed to generate response from local model"
256 ))
257 }
258 }
259
260 pub async fn test_cloud_generation(&self, api_key: &str, model: &str) -> Result<()> {
261 let url = "https://openrouter.ai/api/v1/chat/completions";
262
263 let payload = serde_json::json!({
264 "model": model,
265 "messages": [{"role": "user", "content": "Hello"}],
266 "max_tokens": 1
267 });
268
269 let response = self
270 .client
271 .post(url)
272 .header("Authorization", format!("Bearer {}", api_key))
273 .header("Content-Type", "application/json")
274 .json(&payload)
275 .send()
276 .await?;
277
278 if response.status().is_success() {
279 Ok(())
280 } else {
281 Err(anyhow::anyhow!(
282 "Failed to generate response from cloud model"
283 ))
284 }
285 }
286}
287
288#[derive(Serialize)]
289struct LocalGenerationRequest<'a> {
290 model: &'a str,
291 prompt: &'a str,
292 stream: bool,
293}
294
295#[derive(Deserialize)]
296struct LocalGenerationResponse {
297 response: String,
298}
299
300pub async fn call_local_model(
301 endpoint: &str,
302 model: &str,
303 prompt: &str,
304) -> Result<String, anyhow::Error> {
305 let client = Client::new();
306 let url = if endpoint.starts_with("http") {
307 format!("{}/api/generate", endpoint)
308 } else {
309 format!("http://{}/api/generate", endpoint)
310 };
311
312 let payload = LocalGenerationRequest {
313 model,
314 prompt,
315 stream: false,
316 };
317
318 let response = client.post(&url).json(&payload).send().await?;
319
320 if response.status().is_success() {
321 let gen_response: LocalGenerationResponse = response.json().await?;
322 Ok(gen_response.response)
323 } else {
324 Err(anyhow::anyhow!(
325 "Failed to get response from local model. Status: {}",
326 response.status()
327 ))
328 }
329}
330
331impl Default for ModelValidator {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337fn format_size(bytes: i64) -> String {
338 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
339 let mut size = bytes as f64;
340 let mut unit_index = 0;
341
342 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
343 size /= 1024.0;
344 unit_index += 1;
345 }
346
347 if unit_index == 0 {
348 format!("{} {}", size as i64, UNITS[unit_index])
349 } else {
350 format!("{:.1} {}", size, UNITS[unit_index])
351 }
352}
353
354fn format_relative_time(_iso_time: &str) -> String {
355 "recently".to_string()
358}