use std::sync::Arc;
use ::cordis::Context;
use super::*;
use ares_store::audit_log;
use ares_types::types::{AppError};
use crate::Result;
use crate::HttpError;
use axum::{
Json,
extract::{Path, State},
};
use sha2::Digest;
pub async fn upsert_fleet_provider(
State(ctx): State<Arc<Context>>,
Path(provider_name): Path<String>,
Json(req): Json<FleetProviderUpsertRequest>,
) -> Result<Json<serde_json::Value>> {
if provider_name.is_empty() {
return Err(HttpError::from(AppError::InvalidInput(
"provider_name must not be empty".into()
)));
}
if req.api_key.is_none()
&& req.api_base.is_none()
&& req.default_model.is_none()
&& req.fallback_providers.is_none()
{
return Err(HttpError::from(AppError::InvalidInput(
"At least one of api_key, api_base, default_model, fallback_providers must be provided"
.into()
)));
}
let __pool_1 = ctx.get::<ares_store::TenantDb>().expect("not provided").pool().clone();
let store = fps::FleetProviderSecretsStore::new(&__pool_1);
let master = MasterKey::from_env();
if req.api_key.is_some() && master.is_none() {
return Err(HttpError::from(AppError::Configuration(
"FLEET_SECRETS_KEY is not set; cannot store new API keys. \
Configure /etc/dirmacs/fleet-secrets.env and reload ares.service."
.into()
)));
}
let updated_by = "admin";
let fallback_slice = req.fallback_providers.as_deref();
let stored = store
.upsert(
&provider_name,
req.api_key.as_deref(),
req.api_base.as_deref(),
req.default_model.as_deref(),
fallback_slice,
master.as_ref(),
updated_by,
)
.await?;
let map = store.load_all(master.as_ref()).await?;
ctx.get::<ares_store::FleetSecrets>().expect("not provided").store(map);
let details = serde_json::json!({
"api_key_set": stored.has_api_key,
"api_key_last4": req.api_key.as_deref().and_then(|k| last_n_visible(k, 4)),
"api_base_set": stored.api_base.is_some(),
"default_model_set": stored.default_model.is_some(),
"fallback_providers": stored.fallback_providers,
})
.to_string();
let pool = ctx.get::<ares_store::TenantDb>().expect("not provided").pool().clone();
let name = provider_name.clone();
tokio::spawn(async move {
let _ = audit_log::log_admin_action(
&pool,
"fleet_provider_upsert",
"fleet_provider",
&name,
Some(&details),
None,
)
.await;
});
Ok(Json(serde_json::json!({
"name": provider_name,
"has_api_key": stored.has_api_key,
"api_base": stored.api_base,
"default_model": stored.default_model,
"fallback_providers": stored.fallback_providers,
"updated_at": stored.updated_at,
"updated_by": stored.updated_by,
})))
}
pub async fn delete_fleet_provider(
State(ctx): State<Arc<Context>>,
Path(provider_name): Path<String>,
) -> Result<Json<serde_json::Value>> {
let __pool_2 = ctx.get::<ares_store::TenantDb>().expect("not provided").pool().clone();
let store = fps::FleetProviderSecretsStore::new(&__pool_2);
let affected = store.delete(&provider_name).await?;
if affected == 0 {
return Err(HttpError::from(AppError::NotFound(format!(
"Fleet provider '{}' not found",
provider_name
).into())));
}
let master = MasterKey::from_env();
let map = store.load_all(master.as_ref()).await?;
ctx.get::<ares_store::FleetSecrets>().expect("not provided").store(map);
let pool = ctx.get::<ares_store::TenantDb>().expect("not provided").pool().clone();
let name = provider_name.clone();
tokio::spawn(async move {
let _ = audit_log::log_admin_action(
&pool,
"fleet_provider_delete",
"fleet_provider",
&name,
None,
None,
)
.await;
});
Ok(Json(serde_json::json!({
"name": provider_name,
"deleted": true
})))
}
pub async fn verify_fleet_provider(
State(ctx): State<Arc<Context>>,
Path(provider_name): Path<String>,
) -> Result<Json<FleetProviderVerifyResponse>> {
let start = std::time::Instant::now();
let entry = ctx.get::<ares_store::FleetSecrets>().expect("not provided").get(&provider_name);
let override_ = entry.clone();
let provider_config = ctx.get::<ares_llm::Llm>().expect("not provided").get_provider_for_ctx(&ctx, &provider_name);
let (api_base, api_key) = match (override_, provider_config.as_ref()) {
(Some(o), _) => {
let key = o.api_key.clone();
let base = o
.api_base
.clone()
.or_else(|| provider_config.as_ref().and_then(default_api_base));
(base, key)
}
(None, Some(pc)) => (default_api_base(pc), resolve_env_key(pc)),
(None, None) => (None, None),
};
let api_key = match api_key {
Some(k) if !k.is_empty() => k,
_ => {
return Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: false,
latency_ms: start.elapsed().as_millis() as u64,
model_count: 0,
models: vec![],
error: Some(
"No API key configured for this provider. Set one in the Fleet Providers card."
.into(),
),
}));
}
};
let api_base = match api_base {
Some(b) if !b.is_empty() => b,
_ => {
return Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: false,
latency_ms: start.elapsed().as_millis() as u64,
model_count: 0,
models: vec![],
error: Some("No API base URL configured for this provider.".into()),
}));
}
};
let models_url = format!("{}/models", api_base.trim_end_matches('/'));
let client = reqwest::Client::new();
let resp = match client
.get(&models_url)
.header("Authorization", format!("Bearer {}", api_key))
.header("api-key", &api_key)
.header("Accept", "application/json")
.timeout(std::time::Duration::from_secs(10))
.send()
.await
{
Ok(r) => r,
Err(e) => {
return Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: false,
latency_ms: start.elapsed().as_millis() as u64,
model_count: 0,
models: vec![],
error: Some(format!("HTTP request failed: {e}")),
}));
}
};
let status = resp.status();
let latency_ms = start.elapsed().as_millis() as u64;
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: false,
latency_ms,
model_count: 0,
models: vec![],
error: Some(format!("HTTP {} — {}", status.as_u16(), body)),
}));
}
let parsed: serde_json::Value = match resp.json().await {
Ok(v) => v,
Err(e) => {
return Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: false,
latency_ms,
model_count: 0,
models: vec![],
error: Some(format!("JSON parse failed: {e}")),
}));
}
};
let models: Vec<String> = parsed
.get("data")
.and_then(|d| d.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| item.get("id").and_then(|v| v.as_str()).map(String::from))
.collect()
})
.unwrap_or_default();
let model_count = models.len();
Ok(Json(FleetProviderVerifyResponse {
name: provider_name,
ok: true,
latency_ms,
model_count,
models,
error: None,
}))
}
pub async fn fleet_provider_capabilities() -> Json<FleetProviderCapabilities> {
let providers: Vec<&'static str> = vec!["openai", "azure", "anthropic", "bedrock", "ollama"];
Json(FleetProviderCapabilities {
providers,
encryption_enabled: MasterKey::from_env().is_some(),
})
}
pub fn routes() -> axum::Router<Arc<Context>> {
use axum::routing::{delete, get, post};
axum::Router::new()
.route("/fleet_secrets/list_fleet_providers", get(list_fleet_providers))
.route("/fleet_secrets/upsert_fleet_provider", post(upsert_fleet_provider))
.route("/fleet_secrets/delete_fleet_provider", delete(delete_fleet_provider))
.route("/fleet_secrets/verify_fleet_provider", post(verify_fleet_provider))
.route("/fleet_secrets/fleet_provider_capabilities", get(fleet_provider_capabilities))
}
use ::cordis::Service;