use super::detector;
use super::guide;
use anyhow::Result;
use mermaid_domain::Config;
use mermaid_model::models::adapters::ollama::OllamaAdapter;
use mermaid_model::models::{BackendConfig, Model};
use std::sync::Arc;
async fn list_installed_models(config: &Config) -> Vec<String> {
let backend = BackendConfig {
ollama_url: format!("{}:{}", config.ollama.host, config.ollama.port),
timeout_secs: 5,
max_idle_per_host: 2,
ollama_autostart: config.ollama.auto_start,
};
let autostart = backend.ollama_autostart;
match OllamaAdapter::new("__list__", Arc::new(backend)).await {
Ok(adapter) => {
let adapter = adapter.with_status_notify(Arc::new(|ev| {
if let mermaid_model::models::StreamEvent::Status(text) = ev {
eprintln!("{text}");
}
}));
let adapter = if autostart {
adapter.with_recovery(Arc::new(super::OllamaAutostart))
} else {
adapter
};
adapter.list_models().await.unwrap_or_default()
},
Err(_) => Vec::new(),
}
}
pub async fn ensure_model(model_name: &str, config: &Config) -> Result<()> {
if !detector::is_installed() {
guide::detect_and_guide();
anyhow::bail!("Ollama is not installed. See instructions above.");
}
let model = model_name.strip_prefix("ollama/").unwrap_or(model_name);
if crate::ollama::is_cloud_model(model) {
return Ok(());
}
let models = list_installed_models(config).await;
let model_exists = models
.iter()
.any(|m| m == model || (!model.contains(':') && *m == format!("{model}:latest")));
if !model_exists {
println!("Model '{model}' not found locally. Pulling from Ollama...\n");
let status = std::process::Command::new("ollama")
.arg("pull")
.arg(model)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
match status {
Ok(exit_status) if exit_status.success() => {
println!("\nModel '{model}' pulled successfully.\n");
},
Ok(_) => {
anyhow::bail!(
"Failed to pull model '{model}'. Check if the model name is correct: https://ollama.com/library"
);
},
Err(e) => {
anyhow::bail!("Failed to run 'ollama pull': {e}");
},
}
}
Ok(())
}
pub async fn local_models(config: &Config) -> Option<Vec<String>> {
if !detector::is_installed() {
return None;
}
Some(list_installed_models(config).await)
}
#[cfg(test)]
mod tests {
#[test]
fn cloud_models_skip_pull_local_models_do_not() {
let skips = |name: &str| {
let model = name.strip_prefix("ollama/").unwrap_or(name);
crate::ollama::is_cloud_model(model)
};
for cloud in [
"nemotron-3-ultra:cloud",
"ollama/nemotron-3-ultra:cloud",
"minimax-m3:cloud",
"ollama/gpt-oss:cloud",
] {
assert!(skips(cloud), "{cloud} is a cloud model — must skip pull");
}
for local in ["qwen3:8b", "ollama/llama3.2", "phi3:latest", "mistral"] {
assert!(!skips(local), "{local} is a local model — must still pull");
}
}
}