1use std::path::{Path, PathBuf};
5
6pub const DEFAULT_HTTP: &str = "127.0.0.1:8453";
7
8pub struct Flag {
9 pub name: &'static str,
10 pub value: Option<&'static str>,
11 pub default: &'static str,
12 pub help: &'static str,
13}
14
15pub struct Group {
16 pub title: &'static str,
17 pub flags: &'static [Flag],
18}
19
20const fn f(name: &'static str, value: Option<&'static str>, default: &'static str, help: &'static str) -> Flag {
21 Flag { name, value, default, help }
22}
23
24pub const SERVE: &[Group] = &[
25 Group {
26 title: "Model",
27 flags: &[
28 f("--model", Some("<path|org/model[:tag]>"), "", "A model to serve, repeatable; the first is the default. Also the first bare argument."),
29 f("--runtime", Some("<id>[@<install>]|<model>=<id>"), "by format", "The runtime to serve with (basert, llamacpp, mlx); `auto` clears a pick."),
30 f("--offline", None, "", "Fetch nothing: a model id is only looked for in its runtime's cache."),
31 f("--pull-<name>", Some("[value]"), "", "An option for the runtime that pulls a model id (e.g. --pull-file, --pull-revision)."),
32 f("--<runtime>-<name>", Some("<value>"), "", "A knob of that runtime, e.g. --llamacpp-n-gpu-layers 20; `superfluid runtime options <id>` lists them."),
33 f("--model-dir", Some("<dir>"), "", "Every model in this directory is listed and loaded on first use."),
34 f("--idle-timeout", Some("<sec>"), "0 (never)", "Unload a model that has been idle this long (never the default model)."),
35 f("--basert-lib", Some("<path>"), "search", "Where libbaseRT is, for the basert runtime."),
36 ],
37 },
38 Group {
39 title: "Server",
40 flags: &[
41 f("--http", Some("<addr:port>"), DEFAULT_HTTP, "Serve the OpenAI-, Anthropic- and Ollama-compatible HTTP APIs here."),
42 f("--no-http", None, "", "Serve only the native session API on the unix socket."),
43 f("--host", Some("<addr>"), "127.0.0.1", "The HTTP address, as a host..."),
44 f("--port", Some("<N>"), "8453", "...and a port; either fills the other from its default."),
45 f("--socket", Some("<path>"), "$SUPERFLUID_HOME/superfluid.sock", "The unix socket for the native session API."),
46 f("--sessions", Some("<dir>"), "$SUPERFLUID_HOME/sessions", "The session store (write-ahead log, parks, media, files). One server per directory."),
47 f("--api-key", Some("<key>"), "none", "Require `Authorization: Bearer <key>` (or `X-Api-Key`) on HTTP requests."),
48 f("--key-policy", Some("<file.json>"), "", "Per-key classes, rate and concurrency limits; turns authentication on."),
49 f("--rate-limit", Some("<rpm>"), "0 (off)", "Requests per minute per client IP."),
50 f("--drain-timeout", Some("<sec>"), "60", "On SIGUSR1, stop accepting and give in-flight requests this long."),
51 f("--nonstream-keepalive", Some("<sec>"), "0 (off)", "Send 200 at admission and a space every <sec> until a non-streaming body is ready."),
52 f("--web", Some("<127.0.0.1:port>"), "off", "The browser-facing API, with a token minted to <sessions>/web-token. Loopback only."),
53 f("--web-origin", Some("<url>"), "", "An allowed Origin for --web, repeatable."),
54 ],
55 },
56 Group {
57 title: "Generation",
58 flags: &[
59 f("--max-context", Some("<N|auto>"), "auto", "The context window per lane; auto sizes it for the device where the runtime can, else 8192."),
60 f("--max-batch", Some("<N>"), "8", "Concurrent decode lanes."),
61 f("--max-tokens", Some("<N>"), "fill the context", "The generation cap for requests that set none."),
62 f("--kv-bits", Some("<0|4|8|16|84>"), "0 (auto)", "KV cache precision, on runtimes that take it (basert)."),
63 f("--dialect", Some("<auto|chatml|atem|template|raw>"), "auto", "The chat codec."),
64 f("--tool-call-parser", Some("<auto|json|atem|gemma|harmony|glm>"), "auto", "Override the tool-call format the dialect speaks."),
65 f("--temperature", Some("<F>"), "model's", "Default temperature (0..2) for requests that set none."),
66 f("--top-p", Some("<F>"), "model's", "Default nucleus truncation (0..1)."),
67 f("--top-k", Some("<N>"), "model's", "Default top-k truncation."),
68 f("--min-p", Some("<F>"), "model's", "Default min-p truncation (0..1)."),
69 f("--repeat-penalty", Some("<F>"), "model's", "Default repetition penalty (0..2); 1.0 disables it."),
70 ],
71 },
72 Group {
73 title: "Sessions and caching",
74 flags: &[
75 f("--park", None, "off", "Seal a finishing durable session's KV to disk, to resume it later by id. --no-park turns it off."),
76 f("--park-lossy", None, "off", "Park in the lossy Q8 tier (about half the size); implies --park."),
77 f("--park-budget-gb", Some("<N>"), "20", "Disk budget per park directory, oldest evicted first; 0 is unbounded."),
78 f("--sessions-cache", None, "off", "Treat the session log as a cache: start a fresh one at each start."),
79 f("--tool-lease-ms", Some("<N>"), "0 (none)", "A deadline for every tool call."),
80 f("--prefix-blob-budget-mb", Some("<N>"), "engine's", "The recurrent-state snapshot cache on hybrid models (basert)."),
81 ],
82 },
83 Group {
84 title: "Files API",
85 flags: &[
86 f("--files-max-bytes", Some("<N>"), "unbounded", "Refuse an upload that would push stored bytes past this."),
87 f("--files-expiry", Some("<sec>"), "never", "Delete a stored file this long after upload."),
88 f("--files-sweep", Some("<sec>"), "300", "How often the expiry sweep runs."),
89 ],
90 },
91 Group {
92 title: "Scheduling and QoS",
93 flags: &[
94 f("--tick-decode-budget", Some("<N>"), "max-batch x 32", "Decode tokens granted per tick across all lanes."),
95 f("--tick-target-ms", Some("<N>"), "2000", "The tick length decode grants and adaptive prefill are sized towards."),
96 f("--prefill-budget", Some("<N>"), "4096", "Prompt tokens prefilled per tick (64..4096); 0 adapts it to the tick target."),
97 f("--starvation-ticks", Some("<N>"), "8", "Serve an agent lane first after this many ticks without a slice."),
98 f("--class-lanes", Some("<class>=N[,...]"), "uncapped", "Concurrent-lane cap per QoS class."),
99 f("--http-default-qos", Some("<class>"), "agent", "QoS class for HTTP requests that name none (interactive, completion, agent, background)."),
100 f("--no-http-qos-header", None, "", "Ignore the client's x-superfluid-qos header."),
101 f("--http-allow-batch-invariant", None, "", "Let an HTTP request ask for a batch-invariant lane."),
102 f("--worker-process", None, "on", "Run each engine in its own worker process. --no-worker-process runs it in-process."),
103 f("--os-pressure", None, "on", "Relieve OS memory pressure. --no-os-pressure turns it off."),
104 f("--pressure-high", Some("<pct>"), "85", "KV pool occupancy above which cache is evicted."),
105 f("--pressure-low", Some("<pct>"), "70", "The occupancy eviction brings it down to."),
106 f("--pin-budget-pct", Some("<pct>"), "50", "Share of the KV pool pinned prefixes may hold."),
107 ],
108 },
109 Group {
110 title: "Speculative decoding",
111 flags: &[
112 f("--speculate", Some("<auto|prompt-lookup|mtp-head|<drafter>|off>"), "off", "Draft tokens and verify them; also dflash:, dspark:, eagle3:, draft-model: <path>."),
113 f("--spec-draft-tokens", Some("<N>"), "per strategy", "Draft depth, 1..15."),
114 f("--spec-adaptive", None, "on", "Adapt the depth to the acceptance rate. --no-spec-adaptive pins it."),
115 f("--spec-min-yield", Some("<F>"), "0.75", "The yield below which a lane stops drafting."),
116 f("--spec-yield-rounds", Some("<N>"), "24", "Rounds over which the yield is judged."),
117 f("--spec-throughput-gate", None, "on", "Check that speculation beats plain decoding. --no-spec-throughput-gate skips it."),
118 f("--spec-gate-probe-tokens", Some("<N>"), "16", "The gate's probe length."),
119 f("--spec-min-speedup", Some("<F>"), "1.08", "The speed-up the gate requires."),
120 f("--spec-gate-reprobe", Some("<N>"), "32", "Tokens between re-probes."),
121 f("--spec-gate-reprobe-max", Some("<N>"), "1024", "The ceiling the re-probe interval backs off to."),
122 f("--spec-max-temperature", Some("<F>"), "none", "Cap a model-default temperature on a speculating model."),
123 f("--dspark-confidence", Some("<F>"), "engine's", "The DSpark drafter's confidence threshold."),
124 f("--spec-bitexact", None, "off", "Make speculated and plain decoding bit-exact. --no-spec-bitexact turns it off."),
125 ],
126 },
127 Group {
128 title: "Code completion (FIM)",
129 flags: &[
130 f("--fim-model", Some("<path>"), "", "A dedicated fill-in-the-middle model in its own worker."),
131 f("--fim-max-context", Some("<N>"), "min(window, 8192)", "The FIM model's window."),
132 f("--fim-max-batch", Some("<N>"), "2", "The FIM model's lanes."),
133 f("--completion-deadline-ms", Some("<N>"), "0 (none)", "A completion that cannot start within this expires."),
134 f("--completion-rate", Some("<F>"), "10", "Completions per second per client (0 = unmetered)."),
135 f("--completion-burst", Some("<N>"), "20", "Completions a client may fire back to back."),
136 ],
137 },
138 Group {
139 title: "Logging and telemetry",
140 flags: &[
141 f("--log-filter", Some("<directive>"), "info", "Log level, or per target (superfluid_daemon::scheduler=debug,info)."),
142 f("--verbose", None, "", "The same as --log-filter debug; also -v."),
143 f("--log-dir", Some("<dir>"), "stderr", "Write JSON logs to rotating files here."),
144 f("--log-file", Some("<path>"), "", "Send stderr to this file (appended)."),
145 f("--tui", None, "on a terminal", "The live terminal monitor. --no-tui turns it off."),
146 f("--otlp-metrics", Some("<url>"), "off", "Push scheduler metrics to an OTLP/HTTP collector."),
147 f("--otlp-interval-ms", Some("<N>"), "15000", "The metrics push interval."),
148 f("--otlp-endpoint", Some("<url>"), "off", "Push operational traces (never content) to an OTLP/HTTP collector."),
149 f("--otlp-header", Some("<k=v>"), "", "A header on every OTLP request, repeatable."),
150 f("--otlp-service-name", Some("<name>"), "superfluid", "The service.name resource attribute."),
151 f("--otlp-filter", Some("<directive>"), "info,...scheduler=debug", "Which spans are exported."),
152 f("--otlp-queue", Some("<N>"), "8192", "The span export queue; a full queue drops spans."),
153 f("--otlp-batch-ms", Some("<N>"), "1000", "The span export interval."),
154 f("--otlp-timeout-ms", Some("<N>"), "5000", "The export request timeout."),
155 ],
156 },
157 Group {
158 title: "Fleet head",
159 flags: &[
160 f("--fleet", Some("<host:port>[,...]"), "off", "Serve as a fleet head over these superfluid-noded agents."),
161 f("--fleet-listen", Some("<addr:port>"), "off", "Take nodes that join (`superfluid node join`), announced on the local network."),
162 f("--fleet-auth", Some("<path>"), "$SUPERFLUID_HOME/fleet/token", "The fleet token: the links to the nodes are encrypted with it."),
163 f("--fleet-policy", Some("<load-aware|least-loaded|round-robin>"), "load-aware", "Session placement."),
164 f("--fleet-pool-high", Some("<pct>"), "90", "Pass over a node whose KV pool is at least this full."),
165 f("--fleet-conns-per-node", Some("<N>"), "lanes", "Connections to each node you list; by default as many as it has lanes."),
166 ],
167 },
168];
169
170const SERVE_ALIASES: &[(&str, Option<&str>)] = &[
173 ("--no-park", None),
174 ("--no-worker-process", None),
175 ("--no-os-pressure", None),
176 ("--no-spec-adaptive", None),
177 ("--no-spec-throughput-gate", None),
178 ("--no-spec-bitexact", None),
179 ("--no-tui", None),
180 ("-v", None),
181 ("--help", None),
182 ("-h", None),
183 ("--max-batch-size", Some("--max-batch")),
184 ("--request-timeout", Some("--completion-deadline-ms")),
185];
186
187const SERVE_RETIRED_WITH_VALUE: &[&str] = &[
188 "--prefix-cache-file",
189 "--prefix-cache-save-interval",
190 "--files-dir",
191 "--media-dir",
192 "--metallib",
193 "--prefill-chunk",
194 "--gpu-wait-timeout-ms",
195 "--decode-replay",
196];
197
198const SERVE_RETIRED_SWITCHES: &[&str] =
199 &["--paged-kv", "--prefix-cache", "--continuous-batching", "--paged-weights", "--no-paged-weights-retry", "--no-baked-decode"];
200
201fn serve_flag(name: &str) -> Option<&'static Flag> {
202 SERVE.iter().flat_map(|g| g.flags.iter()).find(|fl| fl.name == name)
203}
204
205pub fn serve_takes_value(name: &str) -> Option<bool> {
208 if let Some(fl) = serve_flag(name) {
209 return Some(fl.value.is_some());
210 }
211 if let Some((_, alias)) = SERVE_ALIASES.iter().find(|(n, _)| *n == name) {
212 return Some(alias.and_then(serve_flag).is_some_and(|fl| fl.value.is_some()));
213 }
214 if SERVE_RETIRED_WITH_VALUE.contains(&name) {
215 return Some(true);
216 }
217 SERVE_RETIRED_SWITCHES.contains(&name).then_some(false)
218}
219
220pub fn split_equals(args: &[String], takes_value: impl Fn(&str) -> Option<bool>) -> Vec<String> {
223 let mut out = Vec::with_capacity(args.len());
224 for a in args {
225 match a.split_once('=') {
226 Some((flag, value)) if flag.starts_with("--") && !flag.starts_with("--pull-") && takes_value(flag) == Some(true) => {
227 out.push(flag.to_string());
228 out.push(value.to_string());
229 }
230 _ => out.push(a.clone()),
231 }
232 }
233 out
234}
235
236fn distance(a: &str, b: &str) -> usize {
237 let b: Vec<char> = b.chars().collect();
238 let mut row: Vec<usize> = (0..=b.len()).collect();
239 for (i, ca) in a.chars().enumerate() {
240 let mut prev = row[0];
241 row[0] = i + 1;
242 for (j, cb) in b.iter().enumerate() {
243 let cur = row[j + 1];
244 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
245 prev = cur;
246 }
247 }
248 row[b.len()]
249}
250
251pub fn suggest<'a>(unknown: &str, known: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
253 let unknown = unknown.split_once('=').map_or(unknown, |(f, _)| f);
254 known
255 .into_iter()
256 .map(|k| (distance(unknown, k), k))
257 .filter(|(d, k)| *d <= 2.max(k.len() / 3) || (unknown.len() > 4 && k.starts_with(unknown)))
258 .min_by_key(|(d, _)| *d)
259 .map(|(_, k)| k)
260}
261
262pub fn serve_flag_names() -> impl Iterator<Item = &'static str> {
263 SERVE.iter().flat_map(|g| g.flags.iter().map(|fl| fl.name)).chain(SERVE_ALIASES.iter().map(|(n, _)| *n)).filter(|n| *n != "--pull-<name>")
264}
265
266pub fn version() -> String {
267 format!("superfluid {}", env!("CARGO_PKG_VERSION"))
268}
269
270pub fn overview() -> String {
271 format!(
272 "{}
273A serving daemon for local LLM inference: one scheduler, one session log and one set of APIs
274over llama.cpp, MLX and baseRT.
275
276Usage: superfluid <command> [options]
277
278Commands:
279 serve <model> Serve a model over HTTP (OpenAI, Anthropic, Ollama) and a unix socket
280 launch <agent> Run a coding agent (Claude Code, ...) against a local model
281 stop Stop the server `launch` started
282 fleet token Print the fleet token, making it the first time
283 node join [<head>] Serve a model to a fleet head from this machine
284 runtimes [<model>] List the inference runtimes here, or which one serves a model
285 runtime <action> Install, update, repair, remove or pin a runtime
286 compat What this build was built for: the runtime contracts it checks at load
287 self <action> Update superfluid itself, or switch between the versions installed here
288 session <action> Inspect or export a session of a running server
289 media <action> Store or garbage-collect media on a running server
290 top <host:port>... Monitor running servers in the terminal
291 help [<command>] Show help for a command
292
293Options:
294 -h, --help Show help
295 -V, --version Show the version
296
297Get started:
298 superfluid serve unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_M # llama.cpp; installed and pulled on first run
299 superfluid serve mlx-community/Qwen3-4B-4bit # MLX on Apple silicon
300 superfluid serve ./model.gguf
301 curl http://{DEFAULT_HTTP}/v1/models
302
303Run 'superfluid <command> --help' for a command's options.
304Docs: https://github.com/basecompute/superfluid/tree/main/docs
305",
306 version()
307 )
308}
309
310pub fn serve_help() -> String {
311 let mut out = String::from(
312 "Serve one or more models.
313
314Usage: superfluid serve <model> [options]
315
316 <model> is a local path (a .gguf file, an MLX directory, a .base bundle) or a Hugging Face
317 style id, org/model[:tag]. An id is pulled by its runtime on first use; the runtime is taken
318 from the id (a *GGUF* repo -> llamacpp, mlx-community/* or *mlx* -> mlx, else basert) unless
319 --runtime names one. A runtime that is not installed is installed first, unless --offline.
320
321Examples:
322 superfluid serve unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_M
323 superfluid serve ./Qwen3-4B-Q4_K_M.gguf --port 8000 --api-key secret
324 superfluid serve --model a.gguf --model b.gguf --max-batch 16
325
326Every option that takes a value accepts --flag value and --flag=value. A runtime's own knobs are
327--<runtime>-<name> <value>; `superfluid runtime options <id>` lists them.
328",
329 );
330 for g in SERVE {
331 out.push_str(&format!("\n{}:\n", g.title));
332 for fl in g.flags {
333 let head = match fl.value {
334 Some(v) => format!("{} {v}", fl.name),
335 None => fl.name.to_string(),
336 };
337 let default = if fl.default.is_empty() { String::new() } else { format!(" [default: {}]", fl.default) };
338 out.push_str(&row(&head, &format!("{}{default}", fl.help)));
339 }
340 }
341 out
342}
343
344const HEAD: usize = 32;
345const WIDTH: usize = 100;
346
347fn row(head: &str, text: &str) -> String {
349 let mut lines: Vec<String> = Vec::new();
350 let mut line = String::new();
351 for word in text.split_whitespace() {
352 if !line.is_empty() && HEAD + 2 + line.len() + 1 + word.len() > WIDTH {
353 lines.push(std::mem::take(&mut line));
354 }
355 if !line.is_empty() {
356 line.push(' ');
357 }
358 line.push_str(word);
359 }
360 lines.push(line);
361 let pad = " ".repeat(HEAD + 2);
362 let mut out = if head.chars().count() + 2 > HEAD {
363 format!(" {head}\n{pad}{}\n", lines[0])
364 } else {
365 format!(" {head:<w$}{}\n", lines[0], w = HEAD)
366 };
367 for l in &lines[1..] {
368 out.push_str(&format!("{pad}{l}\n"));
369 }
370 out
371}
372
373pub const RUNTIMES_HELP: &str = "List the inference runtimes here, or which one serves a model.
374
375Usage: superfluid runtimes [<model>] [--json]
376
377 With no model: one row per runtime and install, its state, version, the newest version its
378 adapter installs on its own here (INSTALLABLE; `superfluid runtime versions <id>` lists them
379 all), the formats it reads and its device, then what to do about any that is not ready.
380 With a model: which runtime reads it and what that runtime serves and refuses (exit 1 if none).
381
382Options:
383 --json Print JSON
384";
385
386pub const RUNTIME_HELP: &str = "Install and manage inference runtimes.
387
388Usage:
389 superfluid runtime versions <id> [--json] List the versions the adapter installs on its own: which install
390 here, as what, and which are installed
391 superfluid runtime install <id> [options] Fetch and install a runtime for this machine
392 superfluid runtime update <id> Install the newest tested build beside the current one
393 superfluid runtime repair <id> [<install>] Reinstall an install in place
394 superfluid runtime remove <id> [<install>] Remove one install, or all of them
395 superfluid runtime use <id> <install>|--auto Pin the default install, or go back to the best-ranked
396 superfluid runtime options <id> [--json] The knobs it takes on `serve`, as --<id>-<name> <value>
397
398 <id> is basert, llamacpp or mlx. Installs live under $SUPERFLUID_HOME/runtimes (~/.superfluid).
399
400Install options:
401 --version <V> A runtime version (default: the newest this adapter was tested with)
402 --backend <B> metal, cuda, vulkan, rocm, cpu, ... (default: the best for this machine)
403 --from <url|path> An archive of your own (for mlx, a package index)
404 --sha256 <H> The archive's digest, checked when given
405 --untested Allow a version the adapter was not tested with
406 --dry-run Print the plan and stop
407
408Examples:
409 superfluid runtime versions llamacpp
410 superfluid runtime install llamacpp
411 superfluid runtime install llamacpp --backend vulkan --dry-run
412";
413
414pub const SESSION_HELP: &str = "Inspect or export a session of a running server.
415
416Usage:
417 superfluid session inspect <id> [--socket <path>] [--json]
418 superfluid session export <id> [--socket <path>] --format jsonl|otlp-jsonl [options]
419
420Options:
421 --socket <path> The server's socket [default: $SUPERFLUID_HOME/superfluid.sock]
422 --json (inspect) Print JSON
423 --format <jsonl|otlp-jsonl> (export) Events as JSON lines, or OTLP trace lines [default: jsonl]
424 --out <dir> (export) Write session-<id>.* here instead of stdout
425 --include-content (export) Include token text
426 --model <name> (export) Label the spans with this model
427 --endpoint <url> (export) Push the traces to an OTLP/HTTP collector instead
428 --allow-content-egress (export) Let --include-content cross the network with --endpoint
429";
430
431pub const MEDIA_HELP: &str = "Store or garbage-collect media on a running server.
432
433Usage:
434 superfluid media put <file> [--socket <path>] Store a file; prints its hash
435 superfluid media gc [--socket <path>] Drop media no session refers to
436
437Options:
438 --socket <path> The server's socket [default: $SUPERFLUID_HOME/superfluid.sock]
439";
440
441pub const TOP_HELP: &str = "Monitor running servers in the terminal.
442
443Usage: superfluid top [--api-key <key>] <host:port> [<host:port> ...]
444
445 Each address is a `superfluid serve` HTTP endpoint; q or Esc quits.
446
447Options:
448 --api-key <key> Sent on every scrape, for servers behind --api-key
449";
450
451pub fn command_help(command: &str) -> Option<String> {
453 Some(match command {
454 "serve" => serve_help(),
455 "runtimes" => RUNTIMES_HELP.to_string(),
456 "runtime" => RUNTIME_HELP.to_string(),
457 "compat" => COMPAT_HELP.to_string(),
458 "self" => crate::selfupdate::HELP.to_string(),
459 "session" => SESSION_HELP.to_string(),
460 "media" => MEDIA_HELP.to_string(),
461 "top" => TOP_HELP.to_string(),
462 "launch" => crate::launch::HELP.to_string(),
463 "fleet" => crate::fleet_token::HELP.to_string(),
464 "node" => crate::fleet_join::HELP.to_string(),
465 "stop" => crate::launch::STOP_HELP.to_string(),
466 _ => return None,
467 })
468}
469
470pub const COMMANDS: &[&str] =
471 &["serve", "launch", "stop", "runtimes", "runtime", "compat", "self", "session", "media", "top", "fleet", "node", "help", "version"];
472
473pub const COMPAT_HELP: &str = "What this build was built and tested for, per runtime.
474
475Usage: superfluid compat [--json]
476
477 basert: the baseRT header version this build carries, the engine ABI number it checks and
478 the line accepted without one (0.3.x), and the engines it installs on its own. A libbaseRT
479 with the same ABI number serves, whatever its version.
480 llamacpp, mlx: the runtime versions each shipped adapter installs on its own, the ones it
481 was built and tested against (`--untested` installs another).
482 A release carries the same data beside its tarball, as compat.json, so a host can choose a
483 release before downloading it. `superfluid runtime versions <id>` lists one runtime's versions
484 with which install on this machine.
485 Exits 1, after printing the rest, when a shipped adapter fails to answer for its runtime.
486
487Options:
488 --json Print JSON
489";
490
491
492pub fn default_sessions(home: &Path) -> PathBuf {
494 home.join("sessions")
495}
496
497pub fn default_socket(home: &Path, sun_path_len: usize) -> PathBuf {
500 let sock = home.join("superfluid.sock");
501 if sock.as_os_str().len() < sun_path_len {
502 return sock;
503 }
504 let uid = unsafe { libc::getuid() };
506 let tmp = std::env::temp_dir().join(format!("superfluid-{uid}.sock"));
507 if tmp.as_os_str().len() < sun_path_len {
508 tmp
509 } else {
510 PathBuf::from(format!("/tmp/superfluid-{uid}.sock"))
511 }
512}
513
514pub fn runtime_for_id(id: &str) -> &'static str {
517 let id = id.split_once(':').map_or(id, |(id, _)| id).to_ascii_lowercase();
518 let (org, repo) = id.split_once('/').unwrap_or(("", &id));
519 if repo.contains("gguf") {
520 "llamacpp"
521 } else if org == "mlx-community" || repo.contains("mlx") {
522 "mlx"
523 } else {
524 "basert"
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 fn strings(v: &[&str]) -> Vec<String> {
533 v.iter().map(|s| s.to_string()).collect()
534 }
535
536 #[test]
537 fn equals_forms_are_split_only_for_flags_that_take_a_value() {
538 let got = split_equals(
539 &strings(&["--port=8000", "--park", "--pull-file=m.gguf", "--model=a=b.gguf", "x=y", "--nope=1", "--no-http=1"]),
540 serve_takes_value,
541 );
542 assert_eq!(got, strings(&["--port", "8000", "--park", "--pull-file=m.gguf", "--model", "a=b.gguf", "x=y", "--nope=1", "--no-http=1"]));
543 assert_eq!(split_equals(&strings(&["--max-batch-size=4"]), serve_takes_value), strings(&["--max-batch-size", "4"]));
544 }
545
546 #[test]
547 fn a_near_miss_is_suggested_and_a_far_one_is_not() {
548 assert_eq!(suggest("--max-ctx", serve_flag_names()), Some("--max-context"));
549 assert_eq!(suggest("--temprature", serve_flag_names()), Some("--temperature"));
550 assert_eq!(suggest("--api_key", serve_flag_names()), Some("--api-key"));
551 assert_eq!(suggest("--sesions=/x", serve_flag_names()), Some("--sessions"));
552 assert_eq!(suggest("--frobnicate-everything", serve_flag_names()), None);
553 }
554
555 #[test]
556 fn a_runtime_is_read_from_a_model_id() {
557 assert_eq!(runtime_for_id("unsloth/Qwen3-0.6B-GGUF:Q4_K_M"), "llamacpp");
558 assert_eq!(runtime_for_id("bartowski/Llama-3.2-1B-Instruct-gguf"), "llamacpp");
559 assert_eq!(runtime_for_id("mlx-community/Qwen3-4B-4bit"), "mlx");
560 assert_eq!(runtime_for_id("someone/Qwen3-4B-MLX-8bit:main"), "mlx");
561 assert_eq!(runtime_for_id("basecompute/Qwen3-1.7B"), "basert");
562 assert_eq!(runtime_for_id("Qwen/Qwen3-0.6B"), "basert");
563 }
564
565 #[test]
566 fn the_default_socket_fits_a_unix_socket_path() {
567 assert_eq!(default_socket(Path::new("/home/u/.superfluid"), 104), Path::new("/home/u/.superfluid/superfluid.sock"));
568 let long = PathBuf::from(format!("/{}", "h".repeat(120)));
569 let s = default_socket(&long, 104);
570 assert!(s.as_os_str().len() < 104 && s.file_name().unwrap().to_string_lossy().starts_with("superfluid-"), "{}", s.display());
571 }
572
573 #[test]
574 fn serve_help_names_every_flag_in_its_table() {
575 let help = serve_help();
576 for name in SERVE.iter().flat_map(|g| g.flags.iter().map(|fl| fl.name)) {
577 assert!(help.contains(name), "{name}");
578 }
579 for line in help.lines() {
580 assert!(line.chars().count() <= WIDTH, "a help line runs on: {line}");
581 }
582 }
583
584 #[test]
587 fn every_flag_serve_reads_is_known_here() {
588 let src = include_str!("serve.rs");
589 let start = src.find("SERVE-FLAGS-BEGIN").expect("marker");
590 let end = src.find("SERVE-FLAGS-END").expect("marker");
591 let body = &src[start..end];
592 let mut seen = 0;
593 for piece in body.split('"').skip(1).step_by(2) {
594 let long = piece.starts_with("--") && piece.len() > 2 && piece != "--pull-";
595 let short = piece.len() == 2 && piece.starts_with('-') && piece.as_bytes()[1].is_ascii_alphabetic();
596 if (long || short) && !piece.contains(' ') {
597 seen += 1;
598 assert!(serve_takes_value(piece).is_some(), "serve reads {piece}, which cli.rs does not list");
599 }
600 }
601 assert!(seen > 100, "the scan found {seen} flags");
602 }
603}