use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub name: String,
pub size: u64,
pub modified_at: String,
}
pub async fn query_available_models(ollama_url: &str) -> Result<Vec<ModelInfo>> {
#[cfg(target_arch = "wasm32")]
{
let _ = ollama_url;
anyhow::bail!("the browser build has no Ollama to query; models come from the page");
}
#[cfg(not(target_arch = "wasm32"))]
{
let client = crate::llm::ollama_client::client_for_endpoint(ollama_url);
let url = format!("{}/api/tags", ollama_url);
let response = client
.get(&url)
.timeout(std::time::Duration::from_secs(5))
.send()
.await
.context("Failed to connect to Ollama API. Is Ollama running?")?;
if !response.status().is_success() {
anyhow::bail!("Ollama API returned error status: {}", response.status());
}
let body = crate::client::http_fetch::read_response_text(response, 8 * 1024 * 1024)
.await
.context("Failed to read response from Ollama")?;
let json: serde_json::Value =
serde_json::from_str(&body).context("Failed to parse Ollama API response")?;
let models = json
.get("models")
.and_then(|m| m.as_array())
.context("Ollama API response missing 'models' array")?;
let mut result = Vec::new();
for model in models {
let name = model
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let size = model.get("size").and_then(|s| s.as_u64()).unwrap_or(0);
let modified_at = model
.get("modified_at")
.and_then(|m| m.as_str())
.unwrap_or("")
.to_string();
if !name.is_empty() {
result.push(ModelInfo {
name,
size,
modified_at,
});
}
}
Ok(result)
}
}
pub fn select_best_model(models: &[ModelInfo]) -> Option<String> {
if models.is_empty() {
return None;
}
let mut sorted = models.to_vec();
sorted.sort_by(|a, b| {
match b.size.cmp(&a.size) {
std::cmp::Ordering::Equal => {
b.modified_at.cmp(&a.modified_at)
}
other => other,
}
});
debug!(
"Available models sorted by size: {:?}",
sorted
.iter()
.map(|m| format!("{} ({} bytes)", m.name, m.size))
.collect::<Vec<_>>()
);
Some(sorted[0].name.clone())
}
pub async fn check_ollama_availability(ollama_url: &str) -> Result<Vec<ModelInfo>> {
query_available_models(ollama_url)
.await
.context("Ollama is not available")
}
pub async fn select_or_validate_model(
configured_model: Option<String>,
interactive: bool,
ollama_url: &str,
) -> Result<Option<String>> {
let models = match check_ollama_availability(ollama_url).await {
Ok(models) => models,
Err(e) => {
let error_msg = format!(
"✗ Ollama is not available at {}: {}\n Please ensure Ollama is running there: https://ollama.ai\n Use `/model` to list and select a model once Ollama is running.",
ollama_url, e
);
if interactive {
warn!("{}", error_msg);
return Ok(None);
} else {
anyhow::bail!("{}", error_msg);
}
}
};
if models.is_empty() {
let error_msg = "✗ No models available in Ollama.\n Please pull a model first: ollama pull qwen2.5-coder:32b\n Use `/model` to list and select a model.";
if interactive {
warn!("{}", error_msg);
return Ok(None);
} else {
anyhow::bail!("{}", error_msg);
}
}
if let Some(ref model_name) = configured_model {
let model_exists = models.iter().any(|m| m.name == *model_name);
if !model_exists {
let available_models = models
.iter()
.map(|m| m.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let error_msg = format!(
"✗ Configured model '{}' not found in Ollama.\n Available models: {}\n Use `/model` to select a different model.",
model_name, available_models
);
if interactive {
warn!("{}", error_msg);
if let Some(best_model) = select_best_model(&models) {
warn!("⚠ Auto-selecting model: {}", best_model);
return Ok(Some(best_model));
}
return Ok(None);
} else {
anyhow::bail!("{}", error_msg);
}
}
info!("✓ Using configured model: {}", model_name);
return Ok(Some(model_name.clone()));
}
if let Some(best_model) = select_best_model(&models) {
warn!(
"⚠ No model configured, auto-selected: {} (largest/most recent)",
best_model
);
info!(" To set a different model, use: /model or edit ~/.netget settings");
Ok(Some(best_model))
} else {
Ok(None)
}
}
pub async fn ensure_model_selected(
current_model: Option<String>,
llm_endpoint: &str,
) -> Result<String> {
if let Some(model) = current_model {
debug!("Model already selected: {}", model);
return Ok(model);
}
warn!(
"⚠ No model selected, attempting to auto-select from models available at {}...",
llm_endpoint
);
match select_or_validate_model(None, false, llm_endpoint).await {
Ok(Some(model)) => {
info!("✓ Auto-selected model: {}", model);
warn!(
"⚠ Auto-selected model: {} (no model was configured)",
model
);
Ok(model)
}
Ok(None) => {
anyhow::bail!(
"✗ No model available at {}.\n Please:\n 1. Ensure Ollama is running there: https://ollama.ai\n 2. Pull a model: ollama pull qwen2.5-coder:32b\n 3. Use `/model` to select a model",
llm_endpoint
)
}
Err(e) => Err(e),
}
}