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