1use std::path::Path;
5use std::sync::OnceLock;
6
7use serde_json::{json, Value};
8use superfluid_adapter_kit::{check_value, placeholder, OptionDecl, WorkerArgs};
9
10use crate::runtime_pick::RuntimeId;
11use crate::runtimes::{Catalog, Runtime, Source, Worker};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Given {
16 pub flag: String,
17 pub runtime: RuntimeId,
18 pub name: String,
19 pub value: String,
20}
21
22pub fn looks_like(flag: &str) -> bool {
25 let Some(body) = flag.strip_prefix("--") else { return false };
26 let id_char = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '.' | '_');
27 let name_char = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-';
28 body.match_indices('-').any(|(i, _)| {
29 let (id, name) = (&body[..i], &body[i + 1..]);
30 !id.is_empty() && id.chars().all(id_char) && !name.is_empty() && name.chars().all(name_char)
31 })
32}
33
34pub fn resolve(flag: &str, value: &str, ids: &[RuntimeId]) -> Option<Given> {
37 let body = flag.strip_prefix("--")?;
38 let mut best: Option<(RuntimeId, &str)> = None;
39 for id in ids.iter().map(|id| id.base()) {
40 let Some(name) = body.strip_prefix(id.name()).and_then(|rest| rest.strip_prefix('-')) else { continue };
41 if !name.is_empty() && best.is_none_or(|(b, _)| id.name().len() > b.name().len()) {
42 best = Some((id, name));
43 }
44 }
45 best.map(|(runtime, name)| Given { flag: flag.to_string(), runtime, name: name.to_string(), value: value.to_string() })
46}
47
48pub fn refuse_repeats(given: &[Given]) -> Result<(), String> {
51 for (i, g) in given.iter().enumerate() {
52 if let Some(first) = given[..i].iter().find(|p| p.runtime == g.runtime && p.name == g.name) {
53 return Err(format!("{}: given twice ({:?}, then {:?}); give it once", g.flag, first.value, g.value));
54 }
55 }
56 Ok(())
57}
58
59pub fn check(given: &[Given], runtime: &Runtime) -> Result<Vec<(String, String)>, String> {
61 let id = runtime.id.base();
62 let mut out = Vec::new();
63 for g in given.iter().filter(|g| g.runtime == id) {
64 let Some(decl) = runtime.options.iter().find(|o| o.name == g.name) else {
65 let takes: Vec<String> = runtime.options.iter().map(|o| format!("--{}-{}", id.name(), o.name)).collect();
66 return Err(if takes.is_empty() {
67 format!("{}: the {} runtime takes no option of its own", g.flag, id.name())
68 } else {
69 format!("{}: the {} runtime takes no such option (it takes: {})", g.flag, id.name(), takes.join(", "))
70 });
71 };
72 check_value(&decl.kind, &decl.choices, decl.least, decl.most, &g.value).map_err(|e| format!("{}: {e}", g.flag))?;
73 out.push((format!("--{}", g.name), g.value.clone()));
74 }
75 Ok(out)
76}
77
78pub type Settled = Vec<(RuntimeId, Vec<(String, String)>)>;
80
81pub fn given_pairs(given: &[Given]) -> Settled {
84 let mut out: Settled = Vec::new();
85 for g in given {
86 let pair = (format!("--{}", g.name), g.value.clone());
87 match out.iter_mut().find(|(id, _)| *id == g.runtime) {
88 Some((_, pairs)) => pairs.push(pair),
89 None => out.push((g.runtime, vec![pair])),
90 }
91 }
92 out
93}
94
95pub fn blamed(catalog: &Catalog, id: RuntimeId, given: &[Given]) -> Option<String> {
98 if !given.iter().any(|g| g.runtime == id.base()) {
99 return None;
100 }
101 let plain = catalog.locate_plain(id)?;
102 let rt = crate::runtimes::check_worker(id, &plain).ok()?;
103 check(given, &rt).err()
104}
105
106pub fn worker_args(id: RuntimeId, model: &Path, ctx: i32, max_batch: u32, kv_bits: i32) -> Result<WorkerArgs, String> {
108 worker_args_from(&for_runtime(id), model, ctx, max_batch, kv_bits)
109}
110
111pub fn pull_args(id: RuntimeId) -> Result<WorkerArgs, String> {
113 let options = for_runtime(id);
114 let argv: Vec<String> = options.iter().flat_map(|(f, v)| [f.clone(), v.clone()]).collect();
115 WorkerArgs::parse(&argv)
116}
117
118pub fn daemon_environment(settled: &Settled) -> Vec<(&'static str, String)> {
121 let mut out = Vec::new();
122 for (id, options) in settled {
123 if id.base().name() == "llamacpp" {
124 if let Some((_, lib)) = options.iter().find(|(flag, _)| flag == "--lib") {
125 out.push(("SUPERFLUID_LLAMA_LIB", lib.clone()));
126 }
127 }
128 }
129 out
130}
131
132pub fn export_to_daemon() {
134 if let Some(all) = SET.get() {
135 for (name, value) in daemon_environment(all) {
136 std::env::set_var(name, value);
137 }
138 }
139}
140
141pub fn worker_args_from(options: &[(String, String)], model: &Path, ctx: i32, max_batch: u32, kv_bits: i32) -> Result<WorkerArgs, String> {
142 let mut argv: Vec<String> = vec![
143 "--model".into(),
144 model.to_string_lossy().into_owned(),
145 "--max-context".into(),
146 ctx.to_string(),
147 "--max-batch".into(),
148 max_batch.to_string(),
149 "--kv-bits".into(),
150 kv_bits.to_string(),
151 ];
152 for (flag, value) in options {
153 argv.push(flag.clone());
154 argv.push(value.clone());
155 }
156 WorkerArgs::parse(&argv)
157}
158
159pub fn settle(given: &[Given], primary: &Runtime, catalog: &Catalog) -> Result<Settled, String> {
161 let mut ids: Vec<RuntimeId> = given.iter().map(|g| g.runtime).collect();
162 ids.sort();
163 ids.dedup();
164 let mut out = Vec::new();
165 for id in ids {
166 let checked = if id == primary.id.base() {
167 check(given, primary)?
168 } else {
169 let rt = catalog.require(id).map_err(|e| {
170 let flag = given.iter().find(|g| g.runtime == id).map(|g| g.flag.as_str()).unwrap_or_default();
171 format!("{flag}: the {} runtime is not usable here, so its options cannot be checked: {e}", id.name())
172 })?;
173 check(given, &rt)?
174 };
175 out.push((id, checked));
176 }
177 Ok(out)
178}
179
180static SET: OnceLock<Settled> = OnceLock::new();
181
182pub fn set(settled: Settled) {
184 let _ = SET.set(settled);
185}
186
187pub fn for_runtime(id: RuntimeId) -> Vec<(String, String)> {
189 SET.get().and_then(|all| all.iter().find(|(r, _)| *r == id.base()).map(|(_, o)| o.clone())).unwrap_or_default()
190}
191
192pub fn declared_by(catalog: &Catalog, id: RuntimeId) -> Result<Vec<OptionDecl>, String> {
196 let id = id.base();
197 let not_usable = match catalog.require(id) {
198 Ok(runtime) => return Ok(runtime.options),
199 Err(why) => why,
200 };
201 let named = std::env::var_os(crate::runtimes::worker_env(id)).is_some();
202 if let Some(rt) = crate::linked::get(id).filter(|_| !named) {
203 return Ok(superfluid_adapter_kit::declared(rt));
204 }
205 let bin = crate::installs::installer(catalog, id).map_err(|_| not_usable.clone())?;
207 let report = crate::runtimes::run_check_model(&Worker { bin, prefix: Vec::new(), source: Source::Shipped }, None).map_err(|_| not_usable)?;
208 Ok(superfluid_worker::check::options_from(&report["options"]))
209}
210
211pub fn text(id: RuntimeId, options: &[OptionDecl]) -> String {
213 let id = id.base().name();
214 if options.is_empty() {
215 return format!("the {id} runtime takes no option of its own; `superfluid serve --help` lists the flags every runtime takes\n");
216 }
217 let rows: Vec<(String, String, String)> = options
218 .iter()
219 .map(|o| {
220 let default = if o.default.is_empty() { "-".to_string() } else { o.default.clone() };
221 (format!("--{id}-{} {}", o.name, placeholder(&o.kind, &o.choices)), default, o.about.clone())
222 })
223 .collect();
224 let w1 = rows.iter().map(|r| r.0.chars().count()).max().unwrap_or(0);
225 let w2 = rows.iter().map(|r| r.1.chars().count()).max().unwrap_or(0).max("DEFAULT".len());
226 let mut out = format!("Options of the {id} runtime on `superfluid serve`:\n\n {:<w1$} {:<w2$} \n", "OPTION", "DEFAULT");
227 for (flag, default, about) in rows {
228 out.push_str(&format!(" {flag:<w1$} {default:<w2$} {about}\n"));
229 }
230 out
231}
232
233pub fn json(id: RuntimeId, options: &[OptionDecl]) -> Value {
235 json!({"runtime": id.base().name(), "options": options.iter().map(|o| o.to_json()).collect::<Vec<_>>()})
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::runtimes::Info;
242
243 fn runtime(id: &str, options: Vec<OptionDecl>) -> Runtime {
244 Runtime {
245 id: RuntimeId::new(id),
246 worker: Worker { bin: std::path::PathBuf::from("/nowhere"), prefix: Vec::new(), source: Source::Env },
247 version: None,
248 abi: None,
249 capabilities: None,
250 info: Info::default(),
251 devices: Vec::new(),
252 tokenizer: None,
253 pull: None,
254 options,
255 }
256 }
257
258 fn decl(name: &str, kind: &str, choices: &[&str], default: &str) -> OptionDecl {
259 OptionDecl {
260 name: name.into(),
261 kind: kind.into(),
262 choices: choices.iter().map(|c| c.to_string()).collect(),
263 least: None,
264 most: None,
265 default: default.into(),
266 about: format!("about {name}"),
267 }
268 }
269
270 #[test]
271 fn the_options_as_given_are_grouped_by_runtime_for_the_first_probe() {
272 let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
273 let given = [
274 resolve("--llamacpp-lib", "/custom/lib", &ids).unwrap(),
275 resolve("--mlx-venv", "/custom/env", &ids).unwrap(),
276 resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap(),
277 ];
278 let pairs = given_pairs(&given);
279 assert_eq!(pairs.len(), 2);
280 assert_eq!(pairs[0], (ids[0], vec![("--lib".to_string(), "/custom/lib".to_string()), ("--n-gpu-layers".to_string(), "20".to_string())]));
281 assert_eq!(pairs[1], (ids[1], vec![("--venv".to_string(), "/custom/env".to_string())]));
282 assert!(given_pairs(&[]).is_empty());
283 }
284
285 #[test]
286 fn an_option_given_twice_is_refused() {
287 let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
288 let given = |flags: &[(&str, &str)]| flags.iter().map(|(f, v)| resolve(f, v, &ids).unwrap()).collect::<Vec<_>>();
289 let twice = given(&[("--llamacpp-lib", "/a"), ("--mlx-venv", "/e"), ("--llamacpp-lib", "/b")]);
290 assert_eq!(refuse_repeats(&twice).unwrap_err(), "--llamacpp-lib: given twice (\"/a\", then \"/b\"); give it once");
291 let same = given(&[("--llamacpp-threads", "8"), ("--llamacpp-threads", "8")]);
292 assert!(refuse_repeats(&same).is_err(), "the same value twice is still a repeat");
293 assert!(refuse_repeats(&given(&[("--llamacpp-lib", "/a"), ("--mlx-venv", "/a"), ("--llamacpp-threads", "8")])).is_ok());
294 assert!(refuse_repeats(&[]).is_ok());
295 }
296
297 #[test]
298 fn in_process_worker_arguments_are_parsed_as_a_worker_process_parses_them() {
299 let options = [("--venv".to_string(), "/custom/env".to_string()), ("--prefill-chunk".to_string(), "512".to_string())];
300 let args = worker_args_from(&options, Path::new("/m"), 4096, 8, 0).unwrap();
301 assert_eq!(args.venv.as_deref(), Some(Path::new("/custom/env")), "where the MLX adapter reads its environment");
302 assert_eq!(args.extra, [("--prefill-chunk".to_string(), "512".to_string())]);
303 assert_eq!((args.model.as_deref(), args.max_context, args.max_batch, args.kv_bits), (Some(Path::new("/m")), 4096, 8, 0));
304 let plain = worker_args_from(&[], Path::new("/m"), 2048, 4, 8).unwrap();
305 assert!(plain.venv.is_none() && plain.extra.is_empty() && plain.kv_bits == 8);
306 assert!(worker_args_from(&[("--lib".to_string(), String::new())], Path::new("/m"), 1, 1, 0).is_ok(), "a value is the parser's to take; the worker judges it");
307 }
308
309 #[test]
310 fn the_daemon_carries_the_llama_cpp_library_for_its_own_tokenizer() {
311 let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
312 let given = [resolve("--llamacpp-lib", "/custom/lib", &ids).unwrap(), resolve("--mlx-venv", "/custom/env", &ids).unwrap()];
313 assert_eq!(daemon_environment(&given_pairs(&given)), [("SUPERFLUID_LLAMA_LIB", "/custom/lib".to_string())]);
314 let other = [resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap()];
315 assert!(daemon_environment(&given_pairs(&other)).is_empty(), "only the library concerns the daemon's own process");
316 let args = WorkerArgs::parse(&["--venv".to_string(), "/custom/env".to_string()]).unwrap();
317 assert_eq!(args.venv.as_deref(), Some(Path::new("/custom/env")), "what pull_args hands an in-process pull");
318 }
319
320 #[test]
321 fn a_flag_is_kept_for_resolution_only_when_shaped_as_an_option() {
322 assert!(looks_like("--llamacpp-n-gpu-layers"));
323 assert!(looks_like("--mlx-venv"));
324 assert!(looks_like("--max-context"), "a flag serve knows never reaches here; the shape alone is judged");
325 assert!(!looks_like("--port"), "no '-' after the runtime");
326 assert!(!looks_like("--llamacpp-"), "an empty name");
327 assert!(!looks_like("-ngl") && !looks_like("--Llamacpp-x") && !looks_like("--llamacpp-x=1"));
328 assert!(!looks_like("--llamacpp-cache_type_k") && !looks_like("--llamacpp-rope.freq"), "no option name holds '_' or '.'");
329 assert!(looks_like("--my_rt.2-threads"), "a runtime id may");
330 assert!(!looks_like("--my_rt.2-"));
331 }
332
333 #[test]
334 fn the_longest_runtime_id_names_the_option() {
335 let ids = [RuntimeId::new("mlx"), RuntimeId::new("mlx-pro"), RuntimeId::new("llamacpp@v1-metal")];
336 let g = resolve("--mlx-pro-chunk", "4", &ids).unwrap();
337 assert_eq!((g.runtime.name(), g.name.as_str(), g.value.as_str()), ("mlx-pro", "chunk", "4"));
338 let g = resolve("--mlx-prefill-chunk", "4", &ids).unwrap();
339 assert_eq!((g.runtime.name(), g.name.as_str()), ("mlx", "prefill-chunk"));
340 let g = resolve("--llamacpp-threads", "8", &ids).unwrap();
341 assert_eq!((g.runtime.name(), g.name.as_str()), ("llamacpp", "threads"), "an install's id names its base runtime");
342 assert!(resolve("--max-contxt", "8", &ids).is_none(), "no runtime max here");
343 assert!(resolve("--mlx-", "8", &ids).is_none());
344 }
345
346 #[test]
347 fn a_value_is_checked_against_the_declaration_and_the_worker_flag_is_built() {
348 let rt = runtime("llamacpp", vec![decl("n-gpu-layers", "int", &[], "-1"), decl("flash-attn", "choice", &["auto", "on", "off"], "auto")]);
349 let ids = [rt.id];
350 let good = [resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap(), resolve("--llamacpp-flash-attn", "off", &ids).unwrap()];
351 assert_eq!(check(&good, &rt).unwrap(), [("--n-gpu-layers".to_string(), "20".to_string()), ("--flash-attn".to_string(), "off".to_string())]);
352 let bad = [resolve("--llamacpp-flash-attn", "yes", &ids).unwrap()];
353 assert_eq!(check(&bad, &rt).unwrap_err(), "--llamacpp-flash-attn: one of auto, on, off expected, got \"yes\"");
354 let mut floored = runtime("llamacpp", vec![decl("batch-size", "int", &[], "2048")]);
355 floored.options[0].least = Some(1);
356 let zero = [resolve("--llamacpp-batch-size", "0", &[floored.id]).unwrap()];
357 assert_eq!(check(&zero, &floored).unwrap_err(), "--llamacpp-batch-size: at least 1 expected, got 0");
358 let mut bounded = runtime("llamacpp", vec![decl("threads", "int", &[], "0"), decl("rope-freq-base", "float", &[], "model's")]);
359 (bounded.options[0].least, bounded.options[0].most, bounded.options[1].least) = (Some(0), Some(512), Some(0));
360 let ids = [bounded.id];
361 let many = [resolve("--llamacpp-threads", "2147483647", &ids).unwrap()];
362 assert_eq!(check(&many, &bounded).unwrap_err(), "--llamacpp-threads: 0 to 512 expected, got 2147483647");
363 let below = [resolve("--llamacpp-rope-freq-base", "-1", &ids).unwrap()];
364 assert_eq!(check(&below, &bounded).unwrap_err(), "--llamacpp-rope-freq-base: at least 0 expected, got -1");
365 let fine = [resolve("--llamacpp-threads", "512", &ids).unwrap(), resolve("--llamacpp-rope-freq-base", "0", &ids).unwrap()];
366 assert_eq!(check(&fine, &bounded).unwrap().len(), 2);
367 let ids = [rt.id];
368 let unknown = [resolve("--llamacpp-ngl", "20", &ids).unwrap()];
369 assert_eq!(
370 check(&unknown, &rt).unwrap_err(),
371 "--llamacpp-ngl: the llamacpp runtime takes no such option (it takes: --llamacpp-n-gpu-layers, --llamacpp-flash-attn)"
372 );
373 let none = runtime("basert", Vec::new());
374 let given = [resolve("--basert-threads", "2", &[none.id]).unwrap()];
375 assert_eq!(check(&given, &none).unwrap_err(), "--basert-threads: the basert runtime takes no option of its own");
376 assert!(check(&good, &none).unwrap().is_empty(), "another runtime's options are not its concern");
377 }
378
379 #[test]
380 fn the_options_are_listed_before_the_runtime_runs_here() {
381 let root = std::env::temp_dir().join(format!("superfluid-runtime-options-declared-{}", std::process::id()));
382 let _ = std::fs::remove_dir_all(&root);
383 let (beside, libexec) = (root.join("bin"), root.join("libexec").join("superfluid"));
384 std::fs::create_dir_all(&beside).unwrap();
385 std::fs::create_dir_all(&libexec).unwrap();
386 let report = r#"{"runtime":{"id":"toyopt","version":"v1"},"available":"no libtoy here","options":[{"name":"lib","kind":"text","default":"the install's","about":"a library of your own"}]}"#;
388 crate::runtimes::tests::fake_worker(&libexec, "superfluid-worker-toyopt", report, 1);
389 let cat = Catalog::new(root.join("home"), Some(beside.clone()), beside.join("superfluid-workerd"));
390 let id = RuntimeId::new("toyopt");
391 assert!(cat.require(id).is_err(), "the runtime does not run here");
392 let options = declared_by(&cat, id).unwrap();
393 assert_eq!(options.iter().map(|o| o.name.as_str()).collect::<Vec<_>>(), ["lib"]);
394 assert!(text(id, &options).contains("--toyopt-lib <value>"));
395 let nowhere = RuntimeId::new("nort");
396 assert_eq!(declared_by(&cat, nowhere).unwrap_err(), cat.require(nowhere).unwrap_err(), "nothing declares it: the runtime's own error");
397 for id in crate::linked::ids().into_iter().filter(|id| std::env::var_os(crate::runtimes::worker_env(*id)).is_none()) {
398 let linked = superfluid_adapter_kit::declared(crate::linked::get(id).unwrap());
399 assert_eq!(declared_by(&cat, id).unwrap(), linked, "{}: a linked adapter answers in process", id.name());
400 }
401 let _ = std::fs::remove_dir_all(&root);
402 }
403
404 #[test]
405 fn the_listing_shows_each_option_with_its_value_shape_and_default() {
406 let rt = runtime("llamacpp", vec![decl("n-gpu-layers", "int", &[], "-1"), decl("flash-attn", "choice", &["auto", "on", "off"], ""), decl("lib", "text", &[], "the install's")]);
407 let t = text(rt.id, &rt.options);
408 assert!(t.contains("--llamacpp-n-gpu-layers <N>"), "{t}");
409 assert!(t.contains("--llamacpp-flash-attn <auto|on|off>"), "{t}");
410 assert!(t.contains("--llamacpp-lib <value>") && t.contains("the install's") && t.contains("about lib"), "{t}");
411 assert!(text(RuntimeId::new("basert"), &[]).starts_with("the basert runtime takes no option of its own"));
412 assert_eq!(json(rt.id, &rt.options)["options"][1]["choices"], json!(["auto", "on", "off"]));
413 }
414}