use std::sync::Arc;
use mermaid_domain::Config;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalModelListing {
Live(Vec<String>),
FromDisk(Vec<String>),
Unreachable,
}
impl LocalModelListing {
#[must_use]
pub fn models(&self) -> Option<&[String]> {
match self {
Self::Live(models) | Self::FromDisk(models) => Some(models),
Self::Unreachable => None,
}
}
}
pub async fn observe_models(config: &Config) -> LocalModelListing {
let live = live_models(config).await;
combine(live, || {
(host_is_loopback(config) && super::is_installed())
.then(super::store::installed_models)
.flatten()
})
}
fn combine(
live: Option<Vec<String>>,
disk: impl FnOnce() -> Option<Vec<String>>,
) -> LocalModelListing {
live.map_or_else(
|| match disk() {
Some(models) if !models.is_empty() => LocalModelListing::FromDisk(models),
_ => LocalModelListing::Unreachable,
},
LocalModelListing::Live,
)
}
async fn live_models(config: &Config) -> Option<Vec<String>> {
use mermaid_model::models::adapters::ollama::OllamaAdapter;
use mermaid_model::models::{BackendConfig, Model};
let backend = BackendConfig {
ollama_url: config.ollama.base_url(),
timeout_secs: 5,
max_idle_per_host: 2,
ollama_autostart: false,
};
match OllamaAdapter::new("__list__", Arc::new(backend)).await {
Ok(adapter) => adapter.list_models().await.ok(),
Err(_) => None,
}
}
fn host_is_loopback(config: &Config) -> bool {
let authority = config.ollama.base_url();
let host = super::server::host_of(super::server::authority_of(&authority));
mermaid_model::utils::classify_host(host).is_loopback()
}
#[cfg(test)]
mod tests {
use super::*;
fn names(list: &[&str]) -> Vec<String> {
list.iter().copied().map(String::from).collect()
}
#[test]
fn combine_prefers_live_then_nonempty_disk() {
assert_eq!(
combine(Some(names(&["a"])), || Some(names(&["b"]))),
LocalModelListing::Live(names(&["a"]))
);
assert_eq!(
combine(Some(Vec::new()), || Some(names(&["b"]))),
LocalModelListing::Live(Vec::new())
);
assert_eq!(
combine(None, || Some(names(&["b"]))),
LocalModelListing::FromDisk(names(&["b"]))
);
assert_eq!(combine(None, || None), LocalModelListing::Unreachable);
assert_eq!(
combine(None, || Some(Vec::new())),
LocalModelListing::Unreachable
);
}
#[test]
fn disk_fallback_is_loopback_only() {
let mut config = Config::default();
assert!(host_is_loopback(&config), "default localhost is loopback");
config.ollama.host = "http://127.0.0.1".to_string();
assert!(host_is_loopback(&config));
config.ollama.host = "ollama.example.com".to_string();
assert!(!host_is_loopback(&config));
config.ollama.host = "http://192.168.1.50".to_string();
assert!(!host_is_loopback(&config), "LAN is not this machine");
}
}