use std::io;
use std::path::PathBuf;
use std::process::{Command, Stdio};
pub struct LmsHarness {
pub binary_path: Option<PathBuf>,
}
impl Default for LmsHarness {
fn default() -> Self {
Self::new()
}
}
impl LmsHarness {
pub fn new() -> Self {
Self {
binary_path: Self::find_lms(),
}
}
fn find_lms() -> Option<PathBuf> {
if let Ok(path) = which::which("lms") {
return Some(path);
}
let home = if cfg!(windows) {
std::env::var("USERPROFILE").ok()
} else {
std::env::var("HOME").ok()
};
if let Some(h) = home {
let bin_name = if cfg!(windows) { "lms.exe" } else { "lms" };
let fallback = PathBuf::from(h)
.join(".lmstudio")
.join("bin")
.join(bin_name);
if fallback.exists() {
return Some(fallback);
}
}
None
}
pub async fn is_server_responding(&self, base_url: &str) -> bool {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(1000))
.build()
.unwrap_or_default();
let url = format!("{}/models", base_url.trim_end_matches('/'));
match client.get(&url).send().await {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
pub fn ensure_server_running(&self) -> io::Result<()> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let status = Command::new(lms)
.args(["server", "start"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(io::Error::other("Failed to start lms server"));
}
Ok(())
}
pub fn list_models(&self) -> io::Result<Vec<String>> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let output = Command::new(lms).args(["ls"]).output()?;
if !output.status.success() {
return Err(io::Error::other("Failed to list models via lms"));
}
let out_str = String::from_utf8_lossy(&output.stdout);
let models = out_str
.lines()
.filter(|l| !l.is_empty() && !l.starts_with("NAME")) .filter_map(|l| l.split_whitespace().next())
.map(|s| s.to_string())
.collect();
Ok(models)
}
pub fn list_loaded_models(&self) -> io::Result<Vec<String>> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let output = Command::new(lms).args(["ps"]).output()?;
if !output.status.success() {
return Err(io::Error::other("Failed to list loaded models via lms"));
}
let out_str = String::from_utf8_lossy(&output.stdout);
let models = out_str
.lines()
.filter(|line| !line.is_empty() && !line.starts_with("NAME"))
.filter_map(|line| line.split_whitespace().next())
.map(|value| value.to_string())
.collect();
Ok(models)
}
pub fn load_model(&self, model_id: &str) -> io::Result<()> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let status = Command::new(lms)
.args(["load", model_id])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"Failed to load model: {}",
model_id
)));
}
Ok(())
}
pub fn unload_model(&self, model_id: &str) -> io::Result<()> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let status = Command::new(lms)
.args(["unload", model_id])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"Failed to unload model: {}",
model_id
)));
}
Ok(())
}
pub fn unload_all_models(&self) -> io::Result<()> {
let Some(ref lms) = self.binary_path else {
return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
};
let status = Command::new(lms)
.args(["unload", "--all"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(io::Error::other("Failed to unload all models"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lms_discovery() {
let harness = LmsHarness::new();
if let Some(path) = harness.binary_path {
assert!(path.exists());
}
}
}