Skip to main content

llm_kernel/discovery/
openai_compat.rs

1use 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
15/// Fetch available model IDs from an OpenAI-compatible endpoint.
16pub 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}