llm_kernel/discovery/
openai_compat.rs1use serde::Deserialize;
2
3use crate::error::{KernelError, Result};
4
5#[derive(Debug, Clone, Deserialize)]
6struct OpenAIModelEntry {
7 id: String,
8}
9
10#[derive(Debug, Clone, Deserialize)]
11struct OpenAIResponse {
12 data: Vec<OpenAIModelEntry>,
13}
14
15pub fn fetch_openai_compatible_models(base_url: &str) -> Result<Vec<String>> {
17 let url = format!("{}/v1/models", 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: OpenAIResponse = resp
25 .body_mut()
26 .read_json()
27 .map_err(KernelError::discovery)?;
28 Ok(payload.data.into_iter().map(|m| m.id).collect())
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn parse_openai_response_multiple_models() {
37 let raw = r#"{"object":"list","data":[{"id":"gpt-4o","object":"model"},{"id":"gpt-4o-mini","object":"model"}]}"#;
38 let resp: OpenAIResponse = serde_json::from_str(raw).unwrap();
39 assert_eq!(resp.data.len(), 2);
40 assert_eq!(resp.data[0].id, "gpt-4o");
41 assert_eq!(resp.data[1].id, "gpt-4o-mini");
42 }
43
44 #[test]
45 fn parse_openai_response_empty() {
46 let raw = r#"{"data":[]}"#;
47 let resp: OpenAIResponse = serde_json::from_str(raw).unwrap();
48 assert!(resp.data.is_empty());
49 }
50
51 #[test]
52 fn parse_model_entry_only_id_required() {
53 let raw = r#"{"id":"mistral-7b-instruct"}"#;
54 let entry: OpenAIModelEntry = serde_json::from_str(raw).unwrap();
55 assert_eq!(entry.id, "mistral-7b-instruct");
56 }
57
58 #[test]
59 fn url_construction_no_double_slash() {
60 let base = "http://localhost:1234/";
61 let expected = "http://localhost:1234/v1/models";
62 let url = format!("{}/v1/models", base.trim_end_matches('/'));
63 assert_eq!(url, expected);
64 }
65
66 #[test]
67 fn parse_lmstudio_style_response() {
68 let raw = r#"{"data":[{"id":"lmstudio-community/gemma-3-4b-it-GGUF"},{"id":"bartowski/Phi-4-mini-instruct-GGUF"}]}"#;
69 let resp: OpenAIResponse = serde_json::from_str(raw).unwrap();
70 assert_eq!(resp.data.len(), 2);
71 assert!(resp.data[0].id.contains('/'));
72 }
73}