use axum::{
extract::{Query, State},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, SystemTime};
use crate::error::ServerError;
use crate::state::AppState;
#[derive(Debug, Clone, Serialize)]
struct ProviderInfo {
id: String,
name: String,
description: String,
command: String,
status: String, source: String, }
#[derive(Debug, Deserialize)]
struct ProvidersQuery {
#[serde(default)]
check: bool,
}
struct Cache {
providers: Option<Vec<ProviderInfo>>,
timestamp: SystemTime,
}
static CACHE: OnceLock<Arc<Mutex<Cache>>> = OnceLock::new();
fn get_cache() -> &'static Arc<Mutex<Cache>> {
CACHE.get_or_init(|| {
Arc::new(Mutex::new(Cache {
providers: None,
timestamp: SystemTime::UNIX_EPOCH,
}))
})
}
const CACHE_TTL: Duration = Duration::from_secs(30);
pub fn router() -> Router<AppState> {
Router::new().route("/", get(list_providers))
}
async fn list_providers(
State(state): State<AppState>,
Query(query): Query<ProvidersQuery>,
) -> Result<Json<serde_json::Value>, ServerError> {
if !query.check {
let _should_return_cached = {
let cache = get_cache().lock().unwrap();
if let Some(ref providers) = cache.providers {
if cache.timestamp.elapsed().unwrap_or(CACHE_TTL) < CACHE_TTL {
return Ok(Json(serde_json::json!({ "providers": providers })));
}
}
false
};
let mut providers = get_providers_without_checking().await;
let docker_status = state.docker_state.detector.check_availability(false).await;
providers.push(ProviderInfo {
id: "docker-opencode".to_string(),
name: "Docker OpenCode".to_string(),
description: if docker_status.available {
"OpenCode in isolated Docker container".to_string()
} else {
"Requires Docker/Colima daemon".to_string()
},
command: "docker run".to_string(),
status: if docker_status.available {
"available".to_string()
} else {
"unavailable".to_string()
},
source: "static".to_string(),
});
return Ok(Json(serde_json::json!({ "providers": providers })));
}
let mut providers = get_providers_with_checking().await;
let docker_status = state.docker_state.detector.check_availability(false).await;
providers.push(ProviderInfo {
id: "docker-opencode".to_string(),
name: "Docker OpenCode".to_string(),
description: if docker_status.available {
"OpenCode in isolated Docker container".to_string()
} else {
"Requires Docker/Colima daemon".to_string()
},
command: "docker run".to_string(),
status: if docker_status.available {
"available".to_string()
} else {
"unavailable".to_string()
},
source: "static".to_string(),
});
{
let mut cache = get_cache().lock().unwrap();
cache.providers = Some(providers.clone());
cache.timestamp = SystemTime::now();
}
Ok(Json(serde_json::json!({ "providers": providers })))
}
fn get_agent_command(agent: &super::acp_registry::RegistryAgent, platform: &str) -> String {
if let Some(npx_val) = agent.distribution.get("npx") {
if let Some(package) = npx_val.get("package").and_then(|v| v.as_str()) {
return format!("npx {}", package);
}
}
if let Some(uvx_val) = agent.distribution.get("uvx") {
if let Some(package) = uvx_val.get("package").and_then(|v| v.as_str()) {
return format!("uvx {}", package);
}
}
if let Some(binary_val) = agent.distribution.get("binary") {
if let Some(platform_bin) = binary_val.get(platform) {
if let Some(cmd) = platform_bin.get("cmd").and_then(|v| v.as_str()) {
return cmd.to_string();
}
}
}
agent.id.clone()
}
async fn get_providers_without_checking() -> Vec<ProviderInfo> {
use crate::acp;
let presets = acp::get_presets();
let mut providers: Vec<ProviderInfo> = presets
.iter()
.map(|p| ProviderInfo {
id: p.id.clone(),
name: p.name.clone(),
description: p.description.clone(),
command: p.command.clone(),
status: "checking".to_string(),
source: "static".to_string(),
})
.collect();
if let Ok(registry) = super::acp_registry::fetch_registry().await {
let static_ids: HashSet<_> = providers.iter().map(|p| p.id.clone()).collect();
let platform =
super::acp_registry::detect_platform().unwrap_or_else(|| "unknown".to_string());
for agent in registry.agents {
let command = get_agent_command(&agent, &platform);
let provider_id = if static_ids.contains(&agent.id) {
format!("{}-registry", agent.id)
} else {
agent.id.clone()
};
let provider_name = if static_ids.contains(&agent.id) {
format!("{} (Registry)", agent.name)
} else {
agent.name.clone()
};
providers.push(ProviderInfo {
id: provider_id,
name: provider_name,
description: agent.description,
command,
status: "checking".to_string(),
source: "registry".to_string(),
});
}
}
providers
}
async fn get_providers_with_checking() -> Vec<ProviderInfo> {
use crate::{acp, shell_env};
let presets = acp::get_presets();
let mut providers: Vec<ProviderInfo> = Vec::new();
for preset in &presets {
let installed = shell_env::which(&preset.command).is_some();
providers.push(ProviderInfo {
id: preset.id.clone(),
name: preset.name.clone(),
description: preset.description.clone(),
command: preset.command.clone(),
status: if installed {
"available".to_string()
} else {
"unavailable".to_string()
},
source: "static".to_string(),
});
}
let static_ids: HashSet<_> = providers.iter().map(|p| p.id.clone()).collect();
if let Ok(registry) = super::acp_registry::fetch_registry().await {
let npx_available = shell_env::which("npx").is_some();
let uvx_available = shell_env::which("uv").is_some();
let platform =
super::acp_registry::detect_platform().unwrap_or_else(|| "unknown".to_string());
for agent in registry.agents {
let (command, status) = if agent.distribution.get("npx").is_some() {
let cmd = get_agent_command(&agent, &platform);
let status_str = if npx_available {
"available"
} else {
"unavailable"
};
(cmd, status_str.to_string())
} else if agent.distribution.get("uvx").is_some() {
let cmd = get_agent_command(&agent, &platform);
let status_str = if uvx_available {
"available"
} else {
"unavailable"
};
(cmd, status_str.to_string())
} else if agent.distribution.get("binary").is_some() {
let cmd = get_agent_command(&agent, &platform);
(cmd, "unavailable".to_string())
} else {
(agent.id.clone(), "unavailable".to_string())
};
let provider_id = if static_ids.contains(&agent.id) {
format!("{}-registry", agent.id)
} else {
agent.id.clone()
};
let provider_name = if static_ids.contains(&agent.id) {
format!("{} (Registry)", agent.name)
} else {
agent.name.clone()
};
providers.push(ProviderInfo {
id: provider_id,
name: provider_name,
description: agent.description,
command,
status,
source: "registry".to_string(),
});
}
}
providers.sort_by(|a, b| {
if a.status == b.status {
a.name.cmp(&b.name)
} else if a.status == "available" {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
});
providers
}