1use std::io;
2use std::path::PathBuf;
3use std::process::{Command, Stdio};
4
5pub struct LmsHarness {
9 pub binary_path: Option<PathBuf>,
10}
11
12impl LmsHarness {
13 pub fn new() -> Self {
14 Self {
15 binary_path: Self::find_lms(),
16 }
17 }
18
19 fn find_lms() -> Option<PathBuf> {
21 if let Ok(path) = which::which("lms") {
23 return Some(path);
24 }
25
26 let home = if cfg!(windows) {
28 std::env::var("USERPROFILE").ok()
29 } else {
30 std::env::var("HOME").ok()
31 };
32
33 if let Some(h) = home {
34 let bin_name = if cfg!(windows) { "lms.exe" } else { "lms" };
35 let fallback = PathBuf::from(h)
36 .join(".lmstudio")
37 .join("bin")
38 .join(bin_name);
39 if fallback.exists() {
40 return Some(fallback);
41 }
42 }
43
44 None
45 }
46
47 pub async fn is_server_responding(&self, base_url: &str) -> bool {
49 let client = reqwest::Client::builder()
50 .timeout(std::time::Duration::from_millis(1000))
51 .build()
52 .unwrap_or_default();
53
54 let url = format!("{}/models", base_url.trim_end_matches('/'));
55 match client.get(&url).send().await {
56 Ok(resp) => resp.status().is_success(),
57 Err(_) => false,
58 }
59 }
60
61 pub fn ensure_server_running(&self) -> io::Result<()> {
63 let Some(ref lms) = self.binary_path else {
64 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
65 };
66
67 let status = Command::new(lms)
70 .args(["server", "start"])
71 .stdout(Stdio::null())
72 .stderr(Stdio::null())
73 .status()?;
74
75 if !status.success() {
76 return Err(io::Error::new(
77 io::ErrorKind::Other,
78 "Failed to start lms server",
79 ));
80 }
81
82 Ok(())
83 }
84
85 pub fn list_models(&self) -> io::Result<Vec<String>> {
87 let Some(ref lms) = self.binary_path else {
88 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
89 };
90
91 let output = Command::new(lms).args(["ls"]).output()?;
92
93 if !output.status.success() {
94 return Err(io::Error::new(
95 io::ErrorKind::Other,
96 "Failed to list models via lms",
97 ));
98 }
99
100 let out_str = String::from_utf8_lossy(&output.stdout);
101 let models = out_str
102 .lines()
103 .filter(|l| !l.is_empty() && !l.starts_with("NAME")) .filter_map(|l| l.split_whitespace().next())
105 .map(|s| s.to_string())
106 .collect();
107
108 Ok(models)
109 }
110
111 pub fn list_loaded_models(&self) -> io::Result<Vec<String>> {
113 let Some(ref lms) = self.binary_path else {
114 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
115 };
116
117 let output = Command::new(lms).args(["ps"]).output()?;
118
119 if !output.status.success() {
120 return Err(io::Error::new(
121 io::ErrorKind::Other,
122 "Failed to list loaded models via lms",
123 ));
124 }
125
126 let out_str = String::from_utf8_lossy(&output.stdout);
127 let models = out_str
128 .lines()
129 .filter(|line| !line.is_empty() && !line.starts_with("NAME"))
130 .filter_map(|line| line.split_whitespace().next())
131 .map(|value| value.to_string())
132 .collect();
133
134 Ok(models)
135 }
136
137 pub fn load_model(&self, model_id: &str) -> io::Result<()> {
139 let Some(ref lms) = self.binary_path else {
140 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
141 };
142
143 let status = Command::new(lms)
144 .args(["load", model_id])
145 .stdout(Stdio::null())
146 .stderr(Stdio::null())
147 .status()?;
148
149 if !status.success() {
150 return Err(io::Error::new(
151 io::ErrorKind::Other,
152 format!("Failed to load model: {}", model_id),
153 ));
154 }
155
156 Ok(())
157 }
158
159 pub fn unload_model(&self, model_id: &str) -> io::Result<()> {
161 let Some(ref lms) = self.binary_path else {
162 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
163 };
164
165 let status = Command::new(lms)
166 .args(["unload", model_id])
167 .stdout(Stdio::null())
168 .stderr(Stdio::null())
169 .status()?;
170
171 if !status.success() {
172 return Err(io::Error::new(
173 io::ErrorKind::Other,
174 format!("Failed to unload model: {}", model_id),
175 ));
176 }
177
178 Ok(())
179 }
180
181 pub fn unload_all_models(&self) -> io::Result<()> {
183 let Some(ref lms) = self.binary_path else {
184 return Err(io::Error::new(io::ErrorKind::NotFound, "lms CLI not found"));
185 };
186
187 let status = Command::new(lms)
188 .args(["unload", "--all"])
189 .stdout(Stdio::null())
190 .stderr(Stdio::null())
191 .status()?;
192
193 if !status.success() {
194 return Err(io::Error::new(
195 io::ErrorKind::Other,
196 "Failed to unload all models",
197 ));
198 }
199
200 Ok(())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_lms_discovery() {
210 let harness = LmsHarness::new();
211 if let Some(path) = harness.binary_path {
213 assert!(path.exists());
214 }
215 }
216}