vtcode_llm/providers/llamacpp/
probe.rs1use serde::{Deserialize, Serialize};
8use std::time::Duration;
9
10use crate::providers::common::override_base_url;
11use crate::providers::ollama::base_url_to_host_root;
12
13use vtcode_config::constants::{env_vars, urls};
14
15use super::LLAMACPP_CONNECTION_ERROR;
16
17#[derive(Debug, Deserialize, Serialize)]
18struct LlamaCppModelsResponse {
19 data: Vec<LlamaCppModel>,
20}
21
22#[derive(Debug, Deserialize, Serialize)]
23struct LlamaCppModel {
24 id: String,
25}
26
27#[derive(Debug)]
28pub(super) enum ServerProbe {
29 Ready(String),
30 Loading,
31 Unavailable(String),
32}
33
34pub async fn fetch_llamacpp_models(base_url: Option<String>) -> Result<Vec<String>, anyhow::Error> {
35 let resolved_base_url = override_base_url(urls::LLAMACPP_API_BASE, base_url, Some(env_vars::LLAMACPP_BASE_URL));
36 let models_url = format!("{}/models", resolved_base_url.trim_end_matches('/'));
37 let client = vtcode_commons::http::create_client_with_timeout(Duration::from_secs(5));
38 let response = client
39 .get(&models_url)
40 .header("Content-Type", "application/json")
41 .send()
42 .await
43 .map_err(|e| {
44 tracing::debug!("Failed to connect to llama.cpp server: {e:?}");
47 anyhow::anyhow!(LLAMACPP_CONNECTION_ERROR)
48 })?;
49
50 if !response.status().is_success() {
51 return Err(anyhow::anyhow!(
52 "Failed to fetch llama.cpp models: HTTP {}. {}",
53 response.status(),
54 if response.status() == reqwest::StatusCode::NOT_FOUND {
55 "Ensure llama-server is running and exposing the OpenAI-compatible /v1 API."
56 } else {
57 ""
58 }
59 ));
60 }
61
62 let models_response: LlamaCppModelsResponse = response
63 .json()
64 .await
65 .map_err(|e| anyhow::anyhow!("Failed to parse llama.cpp models response: {e}"))?;
66
67 Ok(models_response.data.into_iter().map(|model| model.id).collect())
68}
69
70pub(super) async fn probe_server(base_url: &str) -> ServerProbe {
71 let host_root = base_url_to_host_root(base_url);
72 let health_url = format!("{}/health", host_root.trim_end_matches('/'));
73 let client = vtcode_commons::http::create_client_with_timeout(Duration::from_secs(5));
74
75 let response = match client.get(&health_url).send().await {
76 Ok(response) => response,
77 Err(error) => {
78 tracing::debug!("llama.cpp health probe failed for {health_url}: {error}");
79 return ServerProbe::Unavailable(LLAMACPP_CONNECTION_ERROR.to_string());
80 }
81 };
82
83 if response.status().is_success() {
84 return probe_reported_model(base_url).await;
85 }
86
87 let status = response.status();
88 let body = crate::providers::common::read_provider_error_body(response).await;
89 if status == reqwest::StatusCode::SERVICE_UNAVAILABLE && body.to_ascii_lowercase().contains("loading") {
90 return ServerProbe::Loading;
91 }
92
93 if status == reqwest::StatusCode::NOT_FOUND {
94 return probe_reported_model(base_url).await;
95 }
96
97 ServerProbe::Unavailable(format!(
98 "llama.cpp health check failed with HTTP {}{}",
99 status,
100 if body.trim().is_empty() {
101 String::new()
102 } else {
103 format!(": {}", body.trim())
104 }
105 ))
106}
107
108async fn probe_reported_model(base_url: &str) -> ServerProbe {
112 match fetch_llamacpp_models(Some(base_url.to_string())).await {
113 Ok(models) if !models.is_empty() => ServerProbe::Ready(models[0].clone()),
114 Ok(_) => ServerProbe::Unavailable(
115 "llama.cpp is running but did not report any loaded models from /v1/models.".to_string(),
116 ),
117 Err(error) => ServerProbe::Unavailable(error.to_string()),
118 }
119}