use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::providers::common::override_base_url;
use crate::providers::ollama::base_url_to_host_root;
use vtcode_config::constants::{env_vars, urls};
use super::LLAMACPP_CONNECTION_ERROR;
#[derive(Debug, Deserialize, Serialize)]
struct LlamaCppModelsResponse {
data: Vec<LlamaCppModel>,
}
#[derive(Debug, Deserialize, Serialize)]
struct LlamaCppModel {
id: String,
}
#[derive(Debug)]
pub(super) enum ServerProbe {
Ready(String),
Loading,
Unavailable(String),
}
pub async fn fetch_llamacpp_models(base_url: Option<String>) -> Result<Vec<String>, anyhow::Error> {
let resolved_base_url = override_base_url(urls::LLAMACPP_API_BASE, base_url, Some(env_vars::LLAMACPP_BASE_URL));
let models_url = format!("{}/models", resolved_base_url.trim_end_matches('/'));
let client = vtcode_commons::http::create_client_with_timeout(Duration::from_secs(5));
let response = client
.get(&models_url)
.header("Content-Type", "application/json")
.send()
.await
.map_err(|e| {
tracing::warn!("Failed to connect to llama.cpp server: {e:?}");
anyhow::anyhow!(LLAMACPP_CONNECTION_ERROR)
})?;
if !response.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to fetch llama.cpp models: HTTP {}. {}",
response.status(),
if response.status() == reqwest::StatusCode::NOT_FOUND {
"Ensure llama-server is running and exposing the OpenAI-compatible /v1 API."
} else {
""
}
));
}
let models_response: LlamaCppModelsResponse = response
.json()
.await
.map_err(|e| anyhow::anyhow!("Failed to parse llama.cpp models response: {e}"))?;
Ok(models_response.data.into_iter().map(|model| model.id).collect())
}
pub(super) async fn probe_server(base_url: &str) -> ServerProbe {
let host_root = base_url_to_host_root(base_url);
let health_url = format!("{}/health", host_root.trim_end_matches('/'));
let client = vtcode_commons::http::create_client_with_timeout(Duration::from_secs(5));
let response = match client.get(&health_url).send().await {
Ok(response) => response,
Err(error) => {
tracing::debug!("llama.cpp health probe failed for {health_url}: {error}");
return ServerProbe::Unavailable(LLAMACPP_CONNECTION_ERROR.to_string());
}
};
if response.status().is_success() {
return probe_reported_model(base_url).await;
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
if status == reqwest::StatusCode::SERVICE_UNAVAILABLE && body.to_ascii_lowercase().contains("loading") {
return ServerProbe::Loading;
}
if status == reqwest::StatusCode::NOT_FOUND {
return probe_reported_model(base_url).await;
}
ServerProbe::Unavailable(format!(
"llama.cpp health check failed with HTTP {}{}",
status,
if body.trim().is_empty() {
String::new()
} else {
format!(": {}", body.trim())
}
))
}
async fn probe_reported_model(base_url: &str) -> ServerProbe {
match fetch_llamacpp_models(Some(base_url.to_string())).await {
Ok(models) if !models.is_empty() => ServerProbe::Ready(models[0].clone()),
Ok(_) => ServerProbe::Unavailable(
"llama.cpp is running but did not report any loaded models from /v1/models.".to_string(),
),
Err(error) => ServerProbe::Unavailable(error.to_string()),
}
}