use std::time::Duration;
#[cfg(target_os = "linux")]
use std::path::Path;
use super::health::fetch_models_at;
const DISCOVER_CONNECT_TIMEOUT: Duration = Duration::from_millis(600);
const DISCOVER_PROBE_TIMEOUT: Duration = Duration::from_millis(1200);
#[derive(Debug, Clone)]
pub struct EngineCandidate {
pub label: &'static str,
pub base_url: &'static str,
}
pub const ENGINE_CANDIDATES: &[EngineCandidate] = &[
EngineCandidate { label: "ollama", base_url: "http://localhost:11434/v1" },
EngineCandidate { label: "llama.cpp", base_url: "http://localhost:8080/v1" },
EngineCandidate { label: "lm-studio", base_url: "http://localhost:1234/v1" },
EngineCandidate { label: "vllm", base_url: "http://localhost:8000/v1" },
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredEngine {
pub label: String,
pub base_url: String,
pub alive: bool,
pub models: Vec<String>,
}
pub fn discover_engines() -> Vec<DiscoveredEngine> {
let mut candidates: Vec<(String, String)> = ENGINE_CANDIDATES
.iter()
.map(|c| (c.label.to_string(), c.base_url.to_string()))
.collect();
for (label, port) in detect_engine_ports() {
candidates.push((label.to_string(), format!("http://localhost:{port}/v1")));
}
let mut seen = std::collections::HashSet::new();
candidates.retain(|(_, url)| seen.insert(url.clone()));
let handles: Vec<_> = candidates
.into_iter()
.map(|(label, base_url)| {
std::thread::spawn(move || {
match fetch_models_at(&base_url, DISCOVER_CONNECT_TIMEOUT, DISCOVER_PROBE_TIMEOUT) {
Ok(models) => DiscoveredEngine { label, base_url, alive: true, models },
Err(_) => DiscoveredEngine { label, base_url, alive: false, models: Vec::new() },
}
})
})
.collect();
handles.into_iter().filter_map(|h| h.join().ok()).collect()
}
#[cfg(target_os = "linux")]
fn default_port(label: &str) -> u16 {
match label {
"ollama" => 11434,
"llama.cpp" => 8080,
"lm-studio" => 1234,
"vllm" => 8000,
_ => 0,
}
}
#[cfg(target_os = "linux")]
fn match_engine(args: &[String]) -> Option<&'static str> {
let exe = args
.first()
.map(|a| a.rsplit('/').next().unwrap_or(a).to_ascii_lowercase())
.unwrap_or_default();
if exe == "ollama" {
if args.get(1).is_some_and(|a| a == "runner") {
return None;
}
return Some("ollama");
}
if exe == "llama-server" {
return Some("llama.cpp");
}
if exe == "llama" && args.iter().any(|a| a == "serve") {
return Some("llama.cpp");
}
if matches!(exe.as_str(), "bash" | "sh" | "zsh" | "fish" | "dash" | "ksh") {
return None;
}
let joined = args.join(" ").to_ascii_lowercase();
if joined.contains("vllm") {
return Some("vllm");
}
if joined.contains("lm-studio") || joined.contains("lmstudio") {
return Some("lm-studio");
}
None
}
#[cfg(target_os = "linux")]
fn port_from_argv(args: &[String]) -> Option<u16> {
let mut it = args.iter();
while let Some(a) = it.next() {
let parsed = if let Some(rest) = a.strip_prefix("--port=") {
rest.parse().ok()
} else if a == "--port" {
it.next().and_then(|v| v.parse().ok())
} else {
None
};
if let Some(p) = parsed {
return if p == 0 { None } else { Some(p) };
}
}
None
}
#[cfg(target_os = "linux")]
fn detect_engine_ports() -> Vec<(&'static str, u16)> {
struct Hit {
pid: u32,
ppid: Option<u32>,
label: &'static str,
port: u16,
}
let mut hits: Vec<Hit> = Vec::new();
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
for entry in entries.flatten() {
let Some(pid) = entry.file_name().to_str().and_then(|n| n.parse::<u32>().ok()) else {
continue;
};
let Ok(raw) = std::fs::read(entry.path().join("cmdline")) else {
continue;
};
let args: Vec<String> = raw
.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect();
let Some(label) = match_engine(&args) else {
continue;
};
let port = port_from_argv(&args)
.or_else(|| (label == "ollama").then(|| ollama_host_port(&entry.path())).flatten())
.unwrap_or_else(|| default_port(label));
hits.push(Hit { pid, ppid: ppid_of(&entry.path()), label, port });
}
let roots: std::collections::HashMap<u32, &'static str> =
hits.iter().map(|h| (h.pid, h.label)).collect();
let mut found: Vec<(&'static str, u16)> = hits
.iter()
.filter(|h| h.ppid.and_then(|p| roots.get(&p)) != Some(&h.label))
.map(|h| (h.label, h.port))
.collect();
found.sort_unstable();
found.dedup();
found
}
#[cfg(target_os = "linux")]
fn ppid_of(proc_dir: &Path) -> Option<u32> {
let stat = std::fs::read_to_string(proc_dir.join("stat")).ok()?;
let rest = stat.rsplit_once(')')?.1;
rest.split_whitespace().nth(1)?.parse().ok()
}
#[cfg(target_os = "linux")]
fn ollama_host_port(proc_dir: &Path) -> Option<u16> {
let raw = std::fs::read(proc_dir.join("environ")).ok()?;
raw.split(|&b| b == 0).find_map(|kv| {
let kv = String::from_utf8_lossy(kv);
let val = kv.strip_prefix("OLLAMA_HOST=")?;
val.rsplit(':').next()?.parse().ok()
})
}
#[cfg(not(target_os = "linux"))]
fn detect_engine_ports() -> Vec<(&'static str, u16)> {
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_engines_starts_with_every_default_candidate_in_order() {
let found = discover_engines();
assert!(found.len() >= ENGINE_CANDIDATES.len());
for (got, want) in found.iter().zip(ENGINE_CANDIDATES) {
assert_eq!(got.label, want.label);
assert_eq!(got.base_url, want.base_url);
}
for e in &found {
if !e.alive {
assert!(e.models.is_empty(), "dead engine must list no models");
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn match_engine_identifies_servers_without_cross_matching() {
let argv = |s: &str| s.split(' ').map(String::from).collect::<Vec<_>>();
assert_eq!(match_engine(&argv("/usr/bin/llama-server --port 8081 --model x.gguf")), Some("llama.cpp"));
assert_eq!(match_engine(&argv("llama serve --alias grove --port 8081")), Some("llama.cpp"));
assert_eq!(match_engine(&argv("/usr/local/bin/ollama serve")), Some("ollama"));
assert_eq!(match_engine(&argv("llama --help")), None);
assert_eq!(match_engine(&argv("python -m vllm.entrypoints.openai.api_server")), Some("vllm"));
assert_eq!(match_engine(&argv("vim src/main.rs")), None);
assert_eq!(
match_engine(&argv("/usr/local/bin/ollama runner --model m.gguf --port 34371")),
None
);
assert_eq!(
match_engine(&["/bin/bash".into(), "-c".into(), "grep vllm notes.md --port 9000".into()]),
None
);
}
#[cfg(target_os = "linux")]
#[test]
fn port_from_argv_reads_both_spellings_and_ignores_zero() {
let split = ["llama-server".into(), "--port".into(), "8081".into()];
assert_eq!(port_from_argv(&split), Some(8081));
let eq = ["llama-server".into(), "--port=9000".into()];
assert_eq!(port_from_argv(&eq), Some(9000));
let none: [String; 1] = ["llama-server".into()];
assert_eq!(port_from_argv(&none), None);
let zero = ["llama".into(), "serve".into(), "--port".into(), "0".into()];
assert_eq!(port_from_argv(&zero), None);
}
}