Skip to main content

oxicode_ai/providers/
model_fetch.rs

1//! OpenAI-compatible `/v1/models` endpoint model fetching
2//!
3//! Queries the `/v1/models` (or `{base_url}/models`) endpoint of any
4//! OpenAI-compatible provider and returns the list of available model IDs.
5//! Used during startup to auto-register models for custom providers.
6
7use serde::Deserialize;
8
9/// Response shape from the OpenAI `/v1/models` endpoint.
10#[derive(Debug, Deserialize)]
11struct ModelsResponse {
12    data: Vec<ModelInfo>,
13}
14
15/// Individual model entry from the `/v1/models` response.
16#[derive(Debug, Deserialize)]
17struct ModelInfo {
18    id: String,
19
20    _owned_by: Option<String>,
21}
22
23/// Fetch the model list from an OpenAI-compatible `/v1/models` endpoint
24/// using the shared `reqwest::blocking` client.
25///
26/// `base_url` should be something like `"https://api.minimax.chat/v1"`.
27/// The function appends `/models` and issues a `GET` with a `Bearer` token.
28///
29/// # Errors
30///
31/// Returns a human-readable error string on failure (network, auth, parse).
32pub fn fetch_models_blocking(base_url: &str, api_key: &str) -> Result<Vec<String>, String> {
33    // Build the URL: trim trailing slashes, then append "/models"
34    let url = format!("{}/models", base_url.trim_end_matches('/'));
35
36    let client = reqwest::blocking::Client::builder()
37        .timeout(std::time::Duration::from_secs(10))
38        .build()
39        .map_err(|e| format!("failed to build HTTP client: {}", e))?;
40
41    let response = client
42        .get(&url)
43        .header("Authorization", format!("Bearer {}", api_key))
44        .send()
45        .map_err(|e| format!("request to {} failed: {}", url, e))?;
46
47    if !response.status().is_success() {
48        let status = response.status();
49        let body = response.text().unwrap_or_default();
50        return Err(format!("{} returned {}: {}", url, status, body.trim()));
51    }
52
53    let parsed: ModelsResponse = response
54        .json()
55        .map_err(|e| format!("failed to parse models response: {}", e))?;
56
57    Ok(parsed.data.into_iter().map(|m| m.id).collect())
58}
59
60/// Fetch the model list from an OpenAI-compatible `/v1/models` endpoint
61/// asynchronously.
62///
63/// This is the async counterpart to [`fetch_models_blocking`].
64/// Uses the shared async `reqwest::Client` from the provider module.
65///
66/// `base_url` should be something like `"https://api.openai.com/v1"`.
67/// The function appends `/models` and issues a `GET` with a `Bearer` token.
68///
69/// # Errors
70///
71/// Returns a human-readable error string on failure (network, auth, parse).
72pub async fn fetch_models_async(base_url: &str, api_key: &str) -> Result<Vec<String>, String> {
73    use super::shared_client;
74
75    let url = format!("{}/models", base_url.trim_end_matches('/'));
76    let client = shared_client();
77
78    let response = client
79        .get(&url)
80        .header("Authorization", format!("Bearer {}", api_key))
81        .timeout(std::time::Duration::from_secs(10))
82        .send()
83        .await
84        .map_err(|e| format!("request to {} failed: {}", url, e))?;
85
86    if !response.status().is_success() {
87        let status = response.status();
88        let body = response.text().await.unwrap_or_default();
89        return Err(format!("{} returned {}: {}", url, status, body.trim()));
90    }
91
92    let parsed: ModelsResponse = response
93        .json()
94        .await
95        .map_err(|e| format!("failed to parse models response: {}", e))?;
96
97    Ok(parsed.data.into_iter().map(|m| m.id).collect())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_fetch_models_blocking_bad_url() {
106        // Non-routable address → should fail with a network error
107        let result = fetch_models_blocking("http://0.0.0.0:1/v1", "test-key");
108        assert!(result.is_err());
109    }
110}