use reqwest::Client;
use std::time::Duration;
use crate::provider::Provider;
#[derive(Debug, Clone)]
pub struct DetectedProvider {
pub name: String,
pub url: String,
pub model_count: usize,
pub models: Vec<String>,
pub provider: Provider,
}
pub async fn detect(extra_urls: &[&str]) -> Vec<DetectedProvider> {
let client = Client::builder()
.timeout(Duration::from_secs(3))
.build()
.unwrap_or_default();
let mut targets: Vec<(&str, &str)> = Vec::new();
let ollama_url = std::env::var("OLLAMA_HOST")
.unwrap_or_else(|_| "http://localhost:11434".to_string());
let ollama_leaked: &str = Box::leak(ollama_url.into_boxed_str());
targets.push(("Ollama", ollama_leaked));
let lm_url = std::env::var("LM_STUDIO_URL")
.unwrap_or_else(|_| "http://localhost:1234".to_string());
let lm_leaked: &str = Box::leak(lm_url.into_boxed_str());
targets.push(("LM Studio", lm_leaked));
for url in extra_urls {
targets.push(("Custom", url));
}
let mut detected = Vec::new();
for (name, base_url) in &targets {
let base = base_url.trim_end_matches('/');
if let Some(d) = try_ollama(&client, name, base).await {
detected.push(d);
continue;
}
let oai_base = if base.ends_with("/v1") {
base.to_string()
} else {
format!("{}/v1", base)
};
if let Some(d) = try_openai_compat(&client, name, base, &oai_base).await {
detected.push(d);
}
}
detected
}
async fn try_ollama(client: &Client, name: &str, base_url: &str) -> Option<DetectedProvider> {
let url = format!("{}/api/tags", base_url);
let resp = client.get(&url).send().await.ok()?;
let body: serde_json::Value = resp.json().await.ok()?;
let models_arr = body.get("models")?.as_array()?;
let models: Vec<String> = models_arr
.iter()
.filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
.collect();
if models.is_empty() {
return None;
}
Some(DetectedProvider {
name: name.to_string(),
url: base_url.to_string(),
model_count: models.len(),
models,
provider: Provider::ollama(base_url),
})
}
async fn try_openai_compat(
client: &Client,
name: &str,
original_url: &str,
oai_base: &str,
) -> Option<DetectedProvider> {
let url = format!("{}/models", oai_base);
let resp = client.get(&url).send().await.ok()?;
let body: serde_json::Value = resp.json().await.ok()?;
let data = body.get("data")?.as_array()?;
let models: Vec<String> = data
.iter()
.filter_map(|m| m.get("id").and_then(|n| n.as_str()).map(|s| s.to_string()))
.collect();
if models.is_empty() {
return None;
}
Some(DetectedProvider {
name: name.to_string(),
url: original_url.to_string(),
model_count: models.len(),
models,
provider: Provider::openai_compat(oai_base, name),
})
}