1use crate as superfluid_daemon;
5
6use std::os::unix::net::UnixListener;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use superfluid_daemon::runtime_pick::{RuntimeId, RuntimePick};
11use superfluid_daemon::{api, Daemon, EngineHost, SessionStore};
12
13fn retired(flag: &str, why: &str) {
14 eprintln!("superfluid: {flag} is retired — {why}");
15}
16
17fn sampling_f32(flag: &str, raw: &str, lo: f32, hi: f32) -> f32 {
18 let v: f32 = raw.parse().unwrap_or_else(|_| {
19 eprintln!("superfluid: {flag} expects a number, got {raw:?}");
20 std::process::exit(2);
21 });
22 if !v.is_finite() || v < lo || v > hi {
23 eprintln!("superfluid: {flag} must be a finite number in [{lo}, {hi}], got {v}");
24 std::process::exit(2);
25 }
26 v
27}
28
29fn usage_error(msg: &str, command: &str) -> ! {
30 eprintln!("superfluid: {msg}");
31 eprintln!("Run 'superfluid {command} --help' for its options.");
32 std::process::exit(2);
33}
34
35fn needs_value(flag: &str) -> ! {
36 usage_error(&format!("{flag} needs a value"), "serve")
37}
38
39fn num<T: std::str::FromStr>(flag: &str, raw: String) -> T {
40 raw.parse().unwrap_or_else(|_| usage_error(&format!("{flag} expects a number, got {raw:?}"), "serve"))
41}
42
43fn unknown_serve_flag(flag: &str) -> ! {
44 let hint = superfluid_daemon::cli::suggest(flag, superfluid_daemon::cli::serve_flag_names())
45 .map(|s| format!(" (did you mean {s}?)"))
46 .unwrap_or_default();
47 usage_error(&format!("serve has no option {flag}{hint}"), "serve")
48}
49
50fn human_bytes(n: u64) -> String {
51 const GIB: f64 = (1u64 << 30) as f64;
52 const MIB: f64 = (1u64 << 20) as f64;
53 if n as f64 >= GIB {
54 format!("{:.1} GiB", n as f64 / GIB)
55 } else {
56 format!("{:.0} MiB", n as f64 / MIB)
57 }
58}
59
60pub(crate) fn settle_park_dir(dir: &std::path::Path, parking: bool, budget_gb: u64) {
61 if parking {
62 let budget = budget_gb.saturating_mul(1 << 30);
63 if budget == 0 {
64 return;
65 }
66 let freed = superfluid_daemon::park::sweep(dir, budget);
67 if freed > 0 {
68 eprintln!(
69 "superfluid: park retention sweep freed {} in {} at startup (budget {budget_gb} GiB)",
70 human_bytes(freed),
71 dir.display()
72 );
73 }
74 return;
75 }
76 let stale: u64 = std::fs::read_dir(dir)
77 .map(|rd| {
78 rd.filter_map(|e| e.ok())
79 .filter(|e| {
80 let n = e.file_name();
81 let n = n.to_string_lossy();
82 n.ends_with(".park") || n.ends_with(".park.tmp")
83 })
84 .filter_map(|e| e.metadata().ok().map(|m| m.len()))
85 .sum()
86 })
87 .unwrap_or(0);
88 if stale > 0 {
89 eprintln!(
90 "superfluid: {} of park artifacts in {} are from an earlier run with --park; \
91 nothing reads or reclaims them while parking is off. Delete them, or pass \
92 --park to put them back under --park-budget-gb.",
93 human_bytes(stale),
94 dir.display()
95 );
96 }
97}
98
99pub const SUN_PATH_LEN: usize = if cfg!(target_os = "linux") { 108 } else { 104 };
100
101fn listen_or_exit(flag: &str, addr: &str) -> std::net::TcpListener {
102 std::net::TcpListener::bind(addr).unwrap_or_else(|e| {
103 let hint = match e.kind() {
104 std::io::ErrorKind::AddrInUse => " (another process listens there: stop it or pick another port, e.g. --port 8454)",
105 std::io::ErrorKind::AddrNotAvailable => " (no interface on this machine has that address)",
106 std::io::ErrorKind::PermissionDenied => " (a port under 1024 needs privileges)",
107 _ => "",
108 };
109 eprintln!("superfluid: cannot listen on {flag} {addr}: {e}{hint}");
110 std::process::exit(1);
111 })
112}
113
114#[derive(Debug, Clone)]
117pub struct Embedding {
118 pub http: superfluid_daemon::openai::HttpSurface,
120 pub model_naming: superfluid_daemon::openai::ModelNaming,
122 pub model_name: fn(&Path, &std::collections::HashSet<String>) -> String,
126 pub sampling_fallback: Option<superfluid_daemon::openai::SamplingFallback>,
128 pub identity: superfluid_daemon::openai::ServerIdentity,
130 pub session_socket: bool,
133 pub workerd: Option<PathBuf>,
136}
137
138impl Default for Embedding {
139 fn default() -> Embedding {
140 Embedding {
141 http: superfluid_daemon::openai::HttpSurface::default(),
142 model_naming: superfluid_daemon::openai::ModelNaming::default(),
143 model_name: superfluid_daemon::registry::distinct_name,
144 sampling_fallback: None,
145 identity: superfluid_daemon::openai::ServerIdentity::default(),
146 session_socket: true,
147 workerd: None,
148 }
149 }
150}
151
152pub fn run(args: &[String], embedding: &Embedding) {
154 let mut models: Vec<PathBuf> = Vec::new();
155 let mut pull_flags = superfluid_daemon::pulls::PullFlags::default();
156 let mut runtime_pick = RuntimePick::default();
157 let mut api_key: Option<String> = None;
158 let mut rate_limit: u32 = 0;
159 let mut host: Option<String> = None;
160 let mut port: Option<u16> = None;
161 let mut files_max_bytes: Option<u64> = None;
162 let mut files_expiry: Option<u64> = None;
163 let mut files_sweep: u64 = 300;
164 let mut drain_timeout: u64 = 60;
165 let mut nonstream_keepalive: u64 = 0;
166 let mut default_max_tokens: Option<u32> = None;
167 let mut log_file: Option<PathBuf> = None;
168 let mut model_dir: Option<PathBuf> = None;
169 let mut tool_envelope: Option<superfluid_daemon::codec::ToolEnvelope> = None;
170 let mut idle_timeout: u64 = 0;
171 let mut sessions: Option<PathBuf> = None;
172 let mut socket: Option<PathBuf> = None;
173 let mut http: Option<String> = None;
174 let mut no_http = false;
175 let mut max_context: Option<i32> = None;
176 let mut max_context_auto_flag = false;
177 let mut max_batch: u32 = 8;
178 let mut op_temperature: Option<f32> = None;
179 let mut op_top_p: Option<f32> = None;
180 let mut op_top_k: Option<u32> = None;
181 let mut op_min_p: Option<f32> = None;
182 let mut op_repeat_penalty: Option<f32> = None;
183 let mut op_spec_max_temperature: Option<f32> = None;
184 let mut park = false;
185 let mut sessions_cache = false;
186 let mut park_lossy = false;
187 let mut park_budget_gb: u64 = 20;
188 let mut tool_lease_ms: u64 = 0;
189 let mut prefix_blob_budget_mb: u64 = 0;
190 let mut kv_bits_arg: Option<i32> = None;
191 let mut worker_process = true;
192 let mut os_pressure = true;
193 let mut dialect = String::from("auto");
194 let mut pressure_high: u8 = 0;
195 let mut pressure_low: u8 = 0;
196 let mut pin_budget_pct: u8 = superfluid_daemon::DaemonOptions::default().pin_budget_pct;
197 let mut tick_decode_budget: u32 = 0;
198 let mut tick_target_ms: u32 = 0;
199 let mut prefill_budget: u32 = superfluid_daemon::DaemonOptions::default().prefill_budget;
200 let mut starvation_ticks: u32 = 8;
201 let mut class_lanes_spec: Option<String> = None;
202 let mut http_default_qos = superfluid_daemon::openai::RequestQos::AGENT;
203 let mut http_qos_header = true;
204 let mut http_allow_batch_invariant = false;
205 let mut key_policy_path: Option<PathBuf> = None;
206 let mut qos_flags: Vec<&'static str> = Vec::new();
207 let mut log_dir: Option<PathBuf> = None;
208 let mut log_filter = String::from("info");
209 let mut speculate: Option<String> = None;
210 let mut speculation = superfluid_daemon::SpeculationOptions::default();
211 let mut spec_bitexact: Option<bool> = None;
212 let mut completion_deadline_ms: u64 = 0;
213 let mut completion_bucket = superfluid_daemon::completion_bucket::BucketConfig::default();
214 let mut fim_model: Option<PathBuf> = None;
215 let mut fim_max_context: Option<i32> = None;
216 let mut fim_max_batch: u32 = 2;
217 let mut web: Option<String> = None;
218 let mut web_origins: Vec<String> = Vec::new();
219 let mut otlp_metrics: Option<String> = None;
220 let mut otlp_interval_ms: u64 = 15_000;
221 let mut otlp_endpoint: Option<String> = None;
222 let mut otlp_cfg = superfluid_daemon::otlp::OtlpConfig::default();
223 let mut tui_flag = false;
224 let mut no_tui = false;
225 let mut fleet: Option<String> = None;
226 let mut fleet_auth: Option<PathBuf> = None;
227 let mut fleet_listen: Option<String> = None;
228 let mut fleet_policy = String::from("load-aware");
229 let mut fleet_pool_high: u32 = superfluid_daemon::fleet_manager::DEFAULT_POOL_HIGH_PCT;
230 let mut fleet_conns: usize = superfluid_daemon::fleet_manager::DEFAULT_CONNS_PER_NODE;
231 let mut option_candidates: Vec<(String, Option<String>)> = Vec::new();
233 let args = superfluid_daemon::cli::split_equals(args, superfluid_daemon::cli::serve_takes_value);
234 let mut it = args.iter();
235 while let Some(a) = it.next() {
236 let mut val = || it.next().cloned().unwrap_or_else(|| needs_value(a));
237 match a.as_str() {
239 "--model" => models.push(PathBuf::from(val())),
240 "--offline" => pull_flags.offline = true,
241 flag if flag.starts_with("--pull-") => {
242 let spec = &flag["--pull-".len()..];
243 let (name, value) = match spec.split_once('=') {
244 Some((n, v)) => (n.to_string(), Some(v.to_string())),
245 None => (spec.to_string(), it.clone().next().filter(|v| !v.starts_with("--")).cloned()),
246 };
247 if value.is_some() && !spec.contains('=') {
248 it.next();
249 }
250 if name.is_empty() {
251 unknown_serve_flag(a);
252 }
253 pull_flags.options.push((name, value));
254 }
255 "--runtime" => {
256 let v = val();
257 if let Err(e) = runtime_pick.add(&v) {
258 eprintln!("superfluid: --runtime: {e}");
259 std::process::exit(2);
260 }
261 }
262 "--api-key" => api_key = Some(val()),
263 "--rate-limit" => rate_limit = num(a, val()),
264 "--files-max-bytes" => {
265 files_max_bytes = Some(num(a, val()))
266 }
267 "--files-expiry" => files_expiry = Some(num(a, val())),
268 "--files-sweep" => files_sweep = num(a, val()),
269 "--drain-timeout" => drain_timeout = num(a, val()),
270 "--nonstream-keepalive" => {
271 nonstream_keepalive = num(a, val())
272 }
273 "--max-tokens" => {
274 default_max_tokens = Some(num(a, val()))
275 }
276 "--log-file" => log_file = Some(PathBuf::from(val())),
277 "--model-dir" => model_dir = Some(PathBuf::from(val())),
278 "--tool-call-parser" => {
279 let v = val();
280 if v != "auto" {
281 tool_envelope = Some(
282 superfluid_daemon::codec::ToolEnvelope::from_name(&v).unwrap_or_else(|| {
283 eprintln!(
284 "superfluid: unknown --tool-call-parser '{v}' (have: auto, {})",
285 superfluid_daemon::codec::ToolEnvelope::names().join(", ")
286 );
287 std::process::exit(2);
288 }),
289 );
290 }
291 }
292 "--idle-timeout" => idle_timeout = num(a, val()),
293 "--host" => host = Some(val()),
294 "--port" => port = Some(num(a, val())),
295 "--max-batch-size" => max_batch = num(a, val()),
296 "--request-timeout" => {
297 completion_deadline_ms = num(a, val())
298 }
299 "--verbose" | "-v" => log_filter = "debug".into(),
300 "--help" | "-h" => {
301 print!("{}", superfluid_daemon::cli::serve_help());
302 return;
303 }
304 "--sessions" => sessions = Some(PathBuf::from(val())),
305 "--socket" => socket = Some(PathBuf::from(val())),
306 "--http" => http = Some(val()),
307 "--no-http" => no_http = true,
308 "--max-context" => {
309 let v = val();
310 max_context_auto_flag = v == "auto";
311 max_context = if v == "auto" { None } else { Some(num(a, v)) };
312 }
313 "--max-batch" => max_batch = num(a, val()),
314 "--temperature" => op_temperature = Some(sampling_f32("--temperature", &val(), 0.0, 2.0)),
315 "--top-p" => op_top_p = Some(sampling_f32("--top-p", &val(), 0.0, 1.0)),
316 "--top-k" => op_top_k = Some(num(a, val())),
317 "--min-p" => op_min_p = Some(sampling_f32("--min-p", &val(), 0.0, 1.0)),
318 "--repeat-penalty" => {
319 op_repeat_penalty = Some(sampling_f32("--repeat-penalty", &val(), 0.0, 2.0))
320 }
321 "--park" => park = true,
322 "--no-park" => park = false,
323 "--sessions-cache" => sessions_cache = true,
324 "--park-lossy" => {
325 park = true;
326 park_lossy = true;
327 }
328 "--park-budget-gb" => park_budget_gb = num(a, val()),
329 "--tool-lease-ms" => tool_lease_ms = num(a, val()),
330 "--prefix-blob-budget-mb" => {
331 prefix_blob_budget_mb = num(a, val())
332 }
333 "--kv-bits" => kv_bits_arg = Some(num(a, val())),
334 "--dialect" => dialect = val(),
335 "--worker-process" => worker_process = true,
336 "--no-worker-process" => worker_process = false,
337 "--os-pressure" => os_pressure = true,
338 "--no-os-pressure" => os_pressure = false,
339 "--pressure-high" => pressure_high = num(a, val()),
340 "--pressure-low" => pressure_low = num(a, val()),
341 "--pin-budget-pct" => pin_budget_pct = num(a, val()),
342 "--log-dir" => log_dir = Some(PathBuf::from(val())),
343 "--basert-lib" => std::env::set_var("BASERT_LIB", val()),
344 "--speculate" => {
345 let v = val();
346 speculate = if v == "off" { None } else { Some(v) };
347 }
348 "--spec-draft-tokens" => {
349 let n: u32 = num(a, val());
350 if !(1..=15).contains(&n) {
351 eprintln!("superfluid: --spec-draft-tokens must be in 1..=15");
352 std::process::exit(2);
353 }
354 speculation.max_draft = Some(n);
355 }
356 "--spec-adaptive" => speculation.adaptive = Some(true),
357 "--no-spec-adaptive" => speculation.adaptive = Some(false),
358 "--spec-min-yield" => {
359 let v: f64 = num(a, val());
360 if !v.is_finite() || v < 0.0 {
361 eprintln!("superfluid: --spec-min-yield must be finite and non-negative");
362 std::process::exit(2);
363 }
364 speculation.min_yield = Some(v);
365 }
366 "--spec-yield-rounds" => {
367 let n: u32 = num(a, val());
368 if n == 0 {
369 eprintln!("superfluid: --spec-yield-rounds must be at least 1");
370 std::process::exit(2);
371 }
372 speculation.yield_rounds = Some(n);
373 }
374 "--spec-throughput-gate" => speculation.throughput_gate = Some(true),
375 "--no-spec-throughput-gate" => speculation.throughput_gate = Some(false),
376 "--spec-gate-probe-tokens" => {
377 speculation.gate_probe_tokens = Some(num(a, val()))
378 }
379 "--spec-max-temperature" => {
380 op_spec_max_temperature = Some(sampling_f32("--spec-max-temperature", &val(), 0.0, 2.0))
381 }
382 "--spec-min-speedup" => {
383 let v: f64 = num(a, val());
384 if !v.is_finite() || v < 1.0 {
385 eprintln!("superfluid: --spec-min-speedup must be finite and at least 1.0");
386 std::process::exit(2);
387 }
388 speculation.min_speedup = Some(v);
389 }
390 "--spec-gate-reprobe" => {
391 speculation.gate_reprobe = Some(num(a, val()))
392 }
393 "--spec-gate-reprobe-max" => {
394 speculation.gate_reprobe_max = Some(num(a, val()))
395 }
396 "--dspark-confidence" => {
397 let v: f32 = num(a, val());
398 if !v.is_finite() || v <= 0.0 {
399 eprintln!("superfluid: --dspark-confidence must be finite and positive");
400 std::process::exit(2);
401 }
402 speculation.dspark_confidence = Some(v);
403 }
404 "--spec-bitexact" => spec_bitexact = Some(true),
405 "--no-spec-bitexact" => spec_bitexact = Some(false),
406 "--log-filter" => log_filter = val(),
407 "--tick-decode-budget" => {
408 tick_decode_budget = num(a, val())
409 }
410 "--tick-target-ms" => tick_target_ms = num(a, val()),
411 "--prefill-budget" => prefill_budget = num(a, val()),
412 "--starvation-ticks" => starvation_ticks = num(a, val()),
413 "--class-lanes" => {
414 class_lanes_spec = Some(val());
415 qos_flags.push("--class-lanes");
416 }
417 "--http-default-qos" => {
418 qos_flags.push("--http-default-qos");
419 let v = val();
420 http_default_qos.class = superfluid_daemon::qos::parse(&v).unwrap_or_else(|| {
421 eprintln!(
422 "superfluid: --http-default-qos expects one of {}, got {v:?}",
423 superfluid_daemon::qos::NAMES.join(", ")
424 );
425 std::process::exit(2);
426 });
427 }
428 "--no-http-qos-header" => {
429 http_qos_header = false;
430 qos_flags.push("--no-http-qos-header");
431 }
432 "--http-allow-batch-invariant" => {
433 http_allow_batch_invariant = true;
434 qos_flags.push("--http-allow-batch-invariant");
435 }
436 "--key-policy" => key_policy_path = Some(PathBuf::from(val())),
437 "--completion-deadline-ms" => {
438 completion_deadline_ms = num(a, val())
439 }
440 "--completion-rate" => {
441 completion_bucket.rate_per_sec = num(a, val())
442 }
443 "--completion-burst" => {
444 completion_bucket.burst = num(a, val())
445 }
446 "--fim-model" => fim_model = Some(PathBuf::from(val())),
447 "--fim-max-context" => {
448 fim_max_context = Some(num(a, val()))
449 }
450 "--fim-max-batch" => fim_max_batch = num(a, val()),
451 "--web" => web = Some(val()),
452 "--web-origin" => web_origins.push(val()),
453 "--otlp-metrics" => otlp_metrics = Some(val()),
454 "--tui" => tui_flag = true,
455 "--no-tui" => no_tui = true,
456 "--otlp-interval-ms" => otlp_interval_ms = num(a, val()),
457 "--otlp-endpoint" => otlp_endpoint = Some(val()),
458 "--otlp-header" => {
459 let kv = val();
460 let (k, v) = kv.split_once('=').unwrap_or_else(|| usage_error(&format!("--otlp-header expects k=v, got {kv:?}"), "serve"));
461 otlp_cfg.headers.push((k.trim().to_string(), v.trim().to_string()));
462 }
463 "--otlp-service-name" => otlp_cfg.service_name = val(),
464 "--otlp-filter" => otlp_cfg.filter = val(),
465 "--otlp-queue" => otlp_cfg.queue = num(a, val()),
466 "--otlp-batch-ms" => {
467 let ms = num(a, val());
468 otlp_cfg.interval = std::time::Duration::from_millis(ms);
469 }
470 "--otlp-timeout-ms" => {
471 let ms = num(a, val());
472 otlp_cfg.timeout = std::time::Duration::from_millis(ms);
473 }
474 "--fleet" => fleet = Some(val()),
475 "--fleet-auth" => fleet_auth = Some(PathBuf::from(val())),
476 "--fleet-listen" => fleet_listen = Some(val()),
477 "--fleet-policy" => fleet_policy = val(),
478 "--fleet-pool-high" => fleet_pool_high = num(a, val()),
479 "--fleet-conns-per-node" => {
480 fleet_conns = num(a, val())
481 }
482 "--paged-kv" => retired(a, "paged KV is always on"),
483 "--prefix-cache" => retired(a, "prefix reuse is always on"),
484 "--continuous-batching" => {
485 if let Some(n) = it.clone().next().and_then(|v| v.parse::<u32>().ok()) {
486 let _ = it.next();
487 max_batch = n;
488 retired(a, "always on; its width became --max-batch");
489 } else {
490 retired(a, "continuous batching is always on");
491 }
492 }
493 "--prefix-cache-file" | "--prefix-cache-save-interval" => {
494 let _ = val();
495 retired(
496 a,
497 "superseded by the WAL session log (tokens, always durable) and the \
498 in-memory prefix cache; KV on disk is --park, which is OFF by default \
499 and applies to sessions resumed by id through the session API",
500 );
501 }
502 "--files-dir" => {
503 let _ = val();
504 retired(a, "the file store lives under --sessions");
505 }
506 "--media-dir" => {
507 let _ = val();
508 retired(
509 a,
510 "superfluid accepts only data: image URLs and never reads a local path, so there is nothing to allow-list",
511 );
512 }
513 "--prefill-chunk" => {
514 let _ = val();
515 retired(a, "an engine-level knob; a runtime takes its own, --mlx-prefill-chunk or --llamacpp-ubatch-size");
516 }
517 "--metallib" | "--gpu-wait-timeout-ms" | "--decode-replay" => {
518 let _ = val();
519 retired(a, "an engine-level knob superfluid does not expose");
520 }
521 "--paged-weights" | "--no-paged-weights-retry" | "--no-baked-decode" => {
522 retired(a, "an engine-level knob superfluid does not expose")
523 }
524 other if !other.starts_with('-') => models.push(PathBuf::from(other)),
525 _ => {
526 let (flag, given) = match a.split_once('=') {
527 Some((f, v)) => (f, Some(v.to_string())),
528 None => (a.as_str(), None),
529 };
530 if !superfluid_daemon::runtime_options::looks_like(flag) {
531 unknown_serve_flag(a);
532 }
533 let value = match given {
534 Some(v) => Some(v),
535 None => match it.clone().next() {
536 Some(v) if !v.starts_with("--") => {
537 it.next();
538 Some(v.clone())
539 }
540 _ => None,
541 },
542 };
543 option_candidates.push((flag.to_string(), value));
544 }
545 }
546 }
548 if models.is_empty() {
549 usage_error(
550 "serve needs a model: superfluid serve <path|org/model[:tag]>, e.g. \
551 superfluid serve unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_M",
552 "serve",
553 );
554 }
555 superfluid_daemon::speculate_resolve::allow_catalog_fetch(!pull_flags.offline);
556 let catalog = Arc::new(match &embedding.workerd {
557 Some(workerd) => superfluid_daemon::runtimes::Catalog::with_workerd(workerd.clone()),
558 None => superfluid_daemon::runtimes::Catalog::from_env(),
559 });
560 let runtime_options: Vec<superfluid_daemon::runtime_options::Given> = {
561 let ids = catalog.ids();
562 option_candidates
563 .iter()
564 .map(|(flag, value)| match superfluid_daemon::runtime_options::resolve(flag, value.as_deref().unwrap_or_default(), &ids) {
565 None => unknown_serve_flag(flag),
566 Some(_) if value.is_none() => needs_value(flag),
567 Some(given) => given,
568 })
569 .collect()
570 };
571 superfluid_daemon::runtime_options::refuse_repeats(&runtime_options).unwrap_or_else(|e| usage_error(&e, "serve"));
572 superfluid_daemon::runtime_options::set(superfluid_daemon::runtime_options::given_pairs(&runtime_options));
574 superfluid_daemon::runtime_options::export_to_daemon();
576 let sessions = Some(sessions.unwrap_or_else(|| superfluid_daemon::cli::default_sessions(catalog.home())));
577 let socket = if embedding.session_socket {
578 Some(socket.unwrap_or_else(|| superfluid_daemon::cli::default_socket(catalog.home(), SUN_PATH_LEN)))
579 } else {
580 if socket.is_some() {
581 usage_error("--socket: this server has no session socket", "serve");
582 }
583 if no_http {
584 usage_error("--no-http leaves nothing to serve: this server has no session socket", "serve");
585 }
586 None
587 };
588 if no_http && (http.is_some() || host.is_some() || port.is_some()) {
589 usage_error("--no-http contradicts --http/--host/--port", "serve");
590 }
591 let mut pulled_names: std::collections::HashMap<PathBuf, String> = std::collections::HashMap::new();
592 for m in models.iter_mut() {
593 let token = m.to_string_lossy().into_owned();
594 if !superfluid_daemon::pulls::looks_like_id(&token) {
595 continue;
596 }
597 let runtime = match runtime_pick.explicit(m) {
598 Some(name) => catalog.named(name).ok().or_else(|| RuntimeId::parse(name)).unwrap_or_else(|| {
599 eprintln!("superfluid: --runtime '{name}' is not a runtime id");
600 std::process::exit(2);
601 }),
602 None => RuntimeId::new(superfluid_daemon::cli::runtime_for_id(&token)),
603 };
604 if let Err(e) = superfluid_daemon::pulls::ensure_installed(&catalog, runtime, pull_flags.offline) {
606 refuse(e.exit_code(), e.to_string());
607 }
608 let path = superfluid_daemon::pulls::pull(&catalog, runtime, &token, &pull_flags).unwrap_or_else(|e| {
609 eprintln!("superfluid: {e}");
610 std::process::exit(1);
611 });
612 if let Err(e) = runtime_pick.add(&format!("{}={}", path.display(), runtime.name())) {
613 eprintln!("superfluid: {e}");
614 std::process::exit(2);
615 }
616 pulled_names.insert(path.clone(), superfluid_daemon::pulls::served_name(&token));
617 *m = path;
618 }
619 if !pull_flags.options.is_empty() && pulled_names.is_empty() {
620 eprintln!("superfluid: --pull-* options apply to models given by id; no --model here is one");
621 std::process::exit(2);
622 }
623 let (Some(model), Some(sessions)) = (models.first().cloned(), sessions) else {
624 unreachable!("a model and sessions are both set above");
625 };
626 if http.is_none() && !no_http {
627 http = Some(format!(
628 "{}:{}",
629 host.unwrap_or_else(|| "127.0.0.1".into()),
630 port.unwrap_or(8453)
631 ));
632 }
633 if let Some(socket) = socket.as_ref().filter(|s| s.as_os_str().len() >= SUN_PATH_LEN) {
634 eprintln!(
635 "superfluid: --socket {} is {} bytes; a unix socket path must be under {SUN_PATH_LEN} bytes here \
636 (pick a shorter one)",
637 socket.display(),
638 socket.as_os_str().len()
639 );
640 std::process::exit(2);
641 }
642 for (flag, addr) in [("--http", &http), ("--web", &web)] {
643 if let Some(addr) = addr {
644 drop(listen_or_exit(flag, addr));
645 }
646 }
647 let picked = runtime_pick.explicit(&model).is_some();
648 fn refuse(code: i32, e: String) -> ! {
649 eprintln!("superfluid: {e}");
650 std::process::exit(code)
651 }
652 let runtime = match runtime_pick.resolve(&model, &catalog) {
653 Err(e) => refuse(2, if picked { e } else { not_there(&model).unwrap_or(e) }),
654 Ok(rt) => match catalog.require(rt) {
655 Ok(rt) => rt,
656 Err(_) => {
657 if let Some(why) = superfluid_daemon::runtime_options::blamed(&catalog, rt, &runtime_options) {
660 refuse(2, why);
661 }
662 if let Some(why) = not_there(&model) {
663 refuse(2, why);
664 }
665 superfluid_daemon::pulls::ensure_installed(&catalog, rt, pull_flags.offline)
668 .unwrap_or_else(|e| refuse(e.exit_code(), e.to_string()))
669 }
670 },
671 };
672 if let Err(e) = refuse_statically(&runtime, park_lossy, kv_bits_arg, speculate.as_deref()) {
673 refuse(2, e);
674 }
675 superfluid_daemon::runtime_options::settle(&runtime_options, &runtime, &catalog).unwrap_or_else(|e| refuse(2, e));
677 eprintln!("superfluid: runtime {} serves {} ({})", runtime.id.name(), model.display(), describe_worker(&runtime));
678 if let Some(on) = spec_bitexact {
679 std::env::set_var("BASERT_SPEC_BITEXACT", if on { "1" } else { "0" });
680 }
681 let (speculate_primary, speculate_fallbacks): (Option<String>, Vec<String>) = match speculation_for(
682 speculate.as_deref(),
683 &runtime.static_capabilities(),
684 &model,
685 runtime.id.name(),
686 )
687 .map(|d| superfluid_daemon::speculate_resolve::resolve_all(d, &model))
688 {
689 None => (None, Vec::new()),
690 Some(Ok(mut ranked)) => {
691 if let Some(r) = ranked.first() {
692 eprintln!("superfluid: speculation: {r}");
693 } else {
694 let dirs = superfluid_daemon::speculate_resolve::auto_search_dirs(&model);
695 let dirs: Vec<_> = dirs.iter().map(|(d, _)| d.display().to_string()).collect();
696 eprintln!(
697 "superfluid: speculation: auto found no head in {} and no fitting drafter in {} — decoding plainly",
698 model.display(),
699 dirs.join(", ")
700 );
701 }
702 let primary = (!ranked.is_empty()).then(|| ranked.remove(0));
703 (primary, ranked)
704 }
705 Some(Err(e)) => {
706 eprintln!("superfluid: {e}");
707 std::process::exit(2);
708 }
709 };
710 if op_spec_max_temperature.is_some() && speculate.is_none() {
711 eprintln!("superfluid: --spec-max-temperature ignored: no --speculate strategy is configured");
712 }
713 if tui_flag && !cfg!(feature = "tui") {
714 eprintln!(
715 "superfluid: --tui: this build carries no TUI (build superfluid-daemon with the `tui` feature, \
716 which `basert` includes)"
717 );
718 std::process::exit(2);
719 }
720 let class_lanes = match class_lanes_spec.as_deref() {
721 None => [0; 4],
722 Some(spec) => superfluid_daemon::qos::parse_class_lanes(spec, max_batch as usize).unwrap_or_else(|e| {
723 eprintln!("superfluid: --class-lanes: {e}");
724 std::process::exit(2);
725 }),
726 };
727 let key_policy = key_policy_path.as_deref().map(|p| {
728 let table = superfluid_daemon::keypolicy::KeyTable::load(p, api_key.as_deref())
729 .unwrap_or_else(|e| {
730 eprintln!("superfluid: --key-policy {e}");
731 std::process::exit(2);
732 });
733 eprintln!("superfluid: key policy: {} key(s) from {}", table.len(), p.display());
734 Arc::new(table)
735 });
736 if let Err(e) = completion_bucket.validate() {
737 eprintln!("superfluid: --completion-rate/--completion-burst: {e}");
738 std::process::exit(2);
739 }
740 if fim_max_batch == 0 {
741 eprintln!("superfluid: --fim-max-batch must be at least 1");
742 std::process::exit(2);
743 }
744 if sessions_cache && park {
745 eprintln!(
746 "superfluid: --sessions-cache discards the session log at each start, which \
747 --park{} needs to resume parked sessions. Use one or the other.",
748 if park_lossy { "-lossy" } else { "" }
749 );
750 std::process::exit(2);
751 }
752 if models.len() > 1 && !worker_process {
753 eprintln!("superfluid: {} models — enabling --worker-process (each needs its own address space)", models.len());
754 worker_process = true;
755 }
756 if fim_model.is_some() && !worker_process {
757 eprintln!("superfluid: --fim-model needs its own worker — enabling --worker-process");
758 worker_process = true;
759 }
760 if model_dir.is_some() && !worker_process {
761 eprintln!("superfluid: --model-dir needs a per-model worker — enabling --worker-process");
762 worker_process = true;
763 }
764 if let Some(path) = &log_file {
765 use std::os::fd::AsRawFd;
766 match std::fs::OpenOptions::new().create(true).append(true).open(path) {
767 Ok(f) => {
768 unsafe {
772 libc::dup2(f.as_raw_fd(), libc::STDERR_FILENO);
773 }
774 drop(f);
775 }
776 Err(e) => {
777 eprintln!("superfluid: could not open --log-file {}: {e}", path.display());
778 std::process::exit(1);
779 }
780 }
781 }
782 if api_key.is_none() && key_policy.is_none() {
783 let loopback = |addr: &str| {
784 addr.starts_with("localhost:") || addr.parse::<std::net::SocketAddr>().is_ok_and(|a| a.ip().is_loopback())
785 };
786 if http.as_deref().is_none_or(loopback) && web.as_deref().is_none_or(loopback) {
787 eprintln!("superfluid: no --api-key: the HTTP API is open to every process on this machine");
788 } else {
789 eprintln!(
790 "superfluid: WARNING --api-key not set. Authentication is OFF — anyone \
791 who can reach this port can use the API. Bind loopback or set --api-key."
792 );
793 }
794 }
795 let fleet_mode = fleet.is_some() || fleet_listen.is_some();
796 if park && !fleet_mode {
797 let budget = if park_budget_gb == 0 {
798 "NO budget: --park-budget-gb 0 disables the retention sweep, so the \
799 directory grows without limit"
800 .to_string()
801 } else {
802 format!("budget {park_budget_gb} GiB, oldest evicted first")
803 };
804 eprintln!(
805 "superfluid: WARNING --park is on. Every finishing DURABLE session copies its \
806 whole KV off the GPU on the tick thread before it retires — seconds and \
807 gigabytes at agent context, and every other lane waits — then keeps an \
808 artifact under <sessions>/park ({budget}). A /v1/* request mints a fresh \
809 session and can never resume it, so its TERMINAL park is skipped; one \
810 PREEMPTED under load still parks, because it is requeued under the same id \
811 and adopts that artifact. This pays off only for sessions you resume by id \
812 through the session API."
813 );
814 }
815
816 let model_name = pulled_names.get(&model).cloned().unwrap_or_else(|| (embedding.model_name)(&model, &Default::default()));
817 if prefix_blob_budget_mb > 0 {
818 std::env::set_var("BASERT_PREFIX_BLOB_BUDGET_MB", prefix_blob_budget_mb.to_string());
819 }
820 if std::env::var_os("BASERT_YARN").is_some() {
821 eprintln!(
822 "superfluid: WARNING: BASERT_YARN is set but has no effect on this path — the rope \
823 kernels disable YaRN under paged/batched dispatch, which serving always uses. \
824 Long requests run UNSCALED past the trained window."
825 );
826 }
827
828 let tui_on =
829 cfg!(feature = "tui") && !no_tui && (tui_flag || std::io::IsTerminal::is_terminal(&std::io::stdout()));
830 let tui_ring = if tui_on {
831 Some(superfluid_daemon::telemetry::new_log_ring())
832 } else {
833 None
834 };
835 #[cfg(feature = "tui")]
836 let stderr_capture = tui_ring.as_ref().and_then(|ring| {
837 match superfluid_daemon::tui::capture_stderr(ring.clone()) {
838 Ok(guard) => Some(guard),
839 Err(e) => {
840 eprintln!("superfluid: stderr capture unavailable, engine output may overdraw the TUI: {e}");
841 None
842 }
843 }
844 });
845 #[cfg(feature = "tui")]
846 let worker_stderr = stderr_capture.as_ref().map(|c| c.worker_stderr());
847 #[cfg(not(feature = "tui"))]
848 let worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>> = None;
849 let log_dir_eff = log_dir.or(if tui_on { Some(sessions.join("logs")) } else { None });
850 if let Some(url) = &otlp_metrics {
851 if let Err(e) = superfluid_daemon::otlp::validate_http_url(url) {
852 eprintln!("superfluid: --otlp-metrics: {e}");
853 std::process::exit(2);
854 }
855 if otlp_interval_ms == 0 {
856 eprintln!("superfluid: --otlp-interval-ms must be > 0");
857 std::process::exit(2);
858 }
859 }
860 if fleet_mode && otlp_endpoint.is_some() {
861 eprintln!(
862 "superfluid: --otlp-endpoint is not supported in fleet-head mode (the fleet head \
863 has no scheduler to trace); set it on each node's superfluid instead"
864 );
865 std::process::exit(2);
866 }
867 let otlp = otlp_endpoint.map(|endpoint| {
868 let mut cfg = otlp_cfg;
869 cfg.endpoint = endpoint;
870 if let Err(e) = cfg.validate() {
871 eprintln!("superfluid: --otlp-endpoint: {e}");
872 std::process::exit(2);
873 }
874 eprintln!(
875 "superfluid: pushing operational traces to {} ({}an EGRESS: ids, lengths, timings and \
876 codes only, never session content; spans: {})",
877 cfg.traces_url(),
878 if cfg.is_loopback() { "" } else { "NON-LOOPBACK, " },
879 cfg.filter
880 );
881 cfg
882 });
883 use superfluid_daemon::telemetry::{LogConfig, LogSink};
884 let sink = match log_dir_eff {
885 Some(dir) => LogSink::File(LogConfig {
886 dir,
887 filter: log_filter,
888 ..Default::default()
889 }),
890 None => LogSink::Stderr(log_filter),
891 };
892 if let Err(e) = superfluid_daemon::telemetry::init_sinks(sink, tui_ring.clone(), otlp) {
893 eprintln!("superfluid: telemetry: {e}");
894 }
895 std::fs::create_dir_all(&sessions).expect("create sessions dir");
896 let _sessions_lock = if !fleet_mode { Some(lock_sessions_dir(&sessions)) } else { None };
897 if sessions_cache && !fleet_mode {
898 rotate_session_wal(&sessions);
899 if let Ok(rd) = std::fs::read_dir(sessions.join("models")) {
900 for dir in rd.filter_map(|e| e.ok()).map(|e| e.path()).filter(|d| d.is_dir()) {
901 rotate_session_wal(&dir);
902 }
903 }
904 }
905 if !fleet_mode {
906 settle_park_dir(&sessions.join("park"), park, park_budget_gb);
907 if let Ok(rd) = std::fs::read_dir(sessions.join("models")) {
908 let mut dirs: Vec<std::path::PathBuf> = rd
909 .filter_map(|e| e.ok())
910 .map(|e| e.path().join("park"))
911 .filter(|d| d.is_dir())
912 .collect();
913 dirs.sort();
914 for dir in dirs {
915 settle_park_dir(&dir, park, park_budget_gb);
916 }
917 }
918 }
919
920 if let Some(b) = kv_bits_arg {
921 if ![0, 4, 8, 16, 84].contains(&b) {
922 eprintln!("--kv-bits {b}: expected 0 (auto), 4 (q4_0), 8 (q8_0), 16 (f16) or 84 (K q8_0 / V q4_0)");
923 std::process::exit(2);
924 }
925 }
926 let kv_bits = match (park_lossy, kv_bits_arg) {
927 (true, None) | (true, Some(16)) => 16,
928 (true, Some(other)) => {
929 eprintln!("--park-lossy requires F16 KV pools; --kv-bits {other} conflicts");
930 std::process::exit(2);
931 }
932 (false, v) => v.unwrap_or(0),
933 };
934
935 let dir_default = max_context.is_none() && !max_context_auto_flag && model_dir.is_some();
936 let max_context_auto = max_context.is_none() && !dir_default && !fleet_mode;
938 let sizing_lanes = if fim_model.is_some() { max_batch.max(fim_max_batch) } else { max_batch };
939 let mut worker_sized: Option<(u64, u64)> = None;
942 let unsized_why: Option<(String, String)> = if max_context_auto {
943 let sizing = runtime.id.base();
944 let elsewhere = models.iter().chain(fim_model.iter()).find_map(|m| {
945 let on = if fim_model.as_ref() == Some(m) { catalog.reader_of(m) } else { runtime_pick.resolve(m, &catalog) };
946 on.ok().filter(|id| id.base() != sizing).map(|id| (m, id))
947 });
948 match elsewhere {
949 Some((m, id)) => Some((
950 format!(
951 "--max-context auto sizes the window for the {} runtime, and {} is served by {}",
952 sizing.name(),
953 m.display(),
954 id.base().name()
955 ),
956 format!(
957 "not sized for this device: the {} runtime sizes it, and {} is served by {}",
958 sizing.name(),
959 m.display(),
960 id.base().name()
961 ),
962 )),
963 None if !superfluid_daemon::linked::sizes_context(sizing) => {
964 let set: Vec<&PathBuf> = models.iter().chain(fim_model.iter()).collect();
965 let sized = set
966 .iter()
967 .map(|m| superfluid_daemon::runtimes::sizing_of(&runtime, m))
968 .collect::<Result<Vec<_>, String>>()
969 .and_then(|each| {
970 let trained = each.iter().map(|s| s.trained_context).min().unwrap_or(0);
971 superfluid_adapter_kit::sizing::window(&each, sizing_lanes).map(|n| (n, trained)).ok_or_else(|| {
972 format!("the {} runtime reported no device budget to size a window against", sizing.name())
973 })
974 });
975 match sized {
976 Ok(n) => {
977 worker_sized = Some(n);
978 None
979 }
980 Err(why) => Some((format!("--max-context auto: {why}"), why)),
981 }
982 }
983 None => None,
984 }
985 } else {
986 None
987 };
988 let max_context_auto = max_context_auto && unsized_why.is_none();
989 let ctx = if let Some(n) = max_context {
990 n
991 } else if fleet_mode {
992 8192
993 } else if dir_default {
994 eprintln!(
995 "superfluid: context window 8192 tokens (--model-dir loads models on demand at this window, so it is not \
996 sized to fill the device with the startup model; --max-context N|auto overrides)"
997 );
998 8192
999 } else if let Some((asked, kept)) = unsized_why {
1000 if max_context_auto_flag {
1001 eprintln!("superfluid: {asked}: pass --max-context <tokens>");
1002 std::process::exit(2);
1003 }
1004 eprintln!("superfluid: context window 8192 tokens ({kept}; --max-context N pins it)");
1005 8192
1006 } else if let Some((per_lane, trained)) = worker_sized {
1007 let whole = per_lane.saturating_mul(u64::from(sizing_lanes.max(1)));
1009 let n = i32::try_from(whole.min(trained.max(per_lane))).unwrap_or(i32::MAX);
1010 eprintln!(
1011 "superfluid: context window {n} tokens (auto: sized for this device, {per_lane} for each of {sizing_lanes} lanes{}{}, \
1012 one conversation up to the trained {trained}; --max-context N pins it)",
1013 if models.len() > 1 { format!(", {} models", models.len()) } else { String::new() },
1014 if fim_model.is_some() { ", FIM satellite included" } else { "" },
1015 );
1016 n
1017 } else {
1018 use superfluid_daemon::speculate_resolve::{resolve_all, sizing_path, SpecLoad};
1019 let ranked: Vec<Vec<String>> = models
1020 .iter()
1021 .map(|m| {
1022 if *m == model {
1023 speculate_primary.iter().chain(speculate_fallbacks.iter()).cloned().collect()
1024 } else {
1025 speculate.as_deref().and_then(|d| resolve_all(d, m).ok()).unwrap_or_default()
1026 }
1027 })
1028 .collect();
1029 let sizing_models: Vec<PathBuf> = models.iter().cloned().chain(fim_model.iter().cloned()).collect();
1030 let lanes = if fim_model.is_some() { max_batch.max(fim_max_batch) } else { max_batch } as i32;
1031 let speculator_entry = |i: usize, r: &str| -> Option<(PathBuf, bool, Option<usize>)> {
1032 let (path, load) = sizing_path(r, &models[i])?;
1033 Some((path, load == SpecLoad::EmbeddedHead, (load == SpecLoad::Sidecar).then_some(i)))
1034 };
1035 let speculators_for = |choice: &[Option<&String>]| -> Vec<(PathBuf, bool, Option<usize>)> {
1036 (0..models.len()).filter_map(|i| speculator_entry(i, choice[i].or(ranked[i].first())?)).collect()
1037 };
1038 let sizing = runtime.id.base();
1039 let size = |speculators: &[(PathBuf, bool, Option<usize>)]| {
1040 superfluid_daemon::linked::suggest_max_context(sizing, &sizing_models, speculators, lanes, kv_bits).unwrap_or_else(|e| {
1041 eprintln!("superfluid: {e}");
1042 std::process::exit(2);
1043 })
1044 };
1045 let first_picks: Vec<Option<&String>> = vec![None; models.len()];
1046 let n_spec = speculators_for(&first_picks).len();
1047 const MAX_COMBINATIONS: usize = 16;
1048 let counts: Vec<usize> = ranked.iter().map(|c| c.len().max(1)).collect();
1049 let few_enough = counts
1050 .iter()
1051 .try_fold(1usize, |p, &c| p.checked_mul(c).filter(|p| *p <= MAX_COMBINATIONS))
1052 .is_some();
1053 let (sized, note) = if few_enough {
1054 let mut best: Option<(i32, Vec<Option<&String>>)> = None;
1055 let mut idx = vec![0usize; models.len()];
1056 loop {
1057 let choice: Vec<Option<&String>> =
1058 idx.iter().enumerate().map(|(i, &k)| (k > 0).then(|| &ranked[i][k])).collect();
1059 if let Some(n) = size(&speculators_for(&choice)) {
1060 if best.as_ref().is_none_or(|(b, _)| n < *b) {
1061 best = Some((n, choice));
1062 }
1063 }
1064 let mut i = 0;
1065 while i < idx.len() {
1066 idx[i] += 1;
1067 if idx[i] < counts[i] {
1068 break;
1069 }
1070 idx[i] = 0;
1071 i += 1;
1072 }
1073 if i == idx.len() {
1074 break;
1075 }
1076 }
1077 let fallbacks: Vec<&str> =
1078 best.iter().flat_map(|(_, c)| c.iter().flatten().map(|s| s.as_str())).collect();
1079 let note = if fallbacks.is_empty() {
1080 String::new()
1081 } else {
1082 format!(", sized for the fallback {}", fallbacks.join(" + "))
1083 };
1084 (best.map(|(n, _)| n), note)
1085 } else {
1086 let all: Vec<(PathBuf, bool, Option<usize>)> = (0..models.len())
1087 .flat_map(|i| ranked[i].iter().filter_map(move |r| speculator_entry(i, r)))
1088 .collect();
1089 let note = format!(", every one of {} drafter candidates charged at once", all.len());
1090 (size(&all), note)
1091 };
1092 match sized {
1093 Some(n) => {
1094 eprintln!(
1095 "superfluid: context window {n} tokens (auto: sized for this device, {lanes} lanes{}{}{}; --max-context N pins it)",
1096 if n_spec == 0 { String::new() } else { format!(", {n_spec} speculator(s)") },
1097 if fim_model.is_some() { ", FIM satellite included" } else { "" },
1098 note
1099 );
1100 n
1101 }
1102 None => {
1103 eprintln!(
1104 "superfluid: context window 8192 tokens (auto could not size this device/model set; --max-context N pins it)"
1105 );
1106 8192
1107 }
1108 }
1109 }
1110 .max(512);
1111 if let Some(n) = max_context {
1112 eprintln!("superfluid: context window {ctx} tokens (--max-context {n})");
1113 }
1114 let spawns_worker = !runtime.in_process_capable() || (worker_process && runtime.worker.bin.is_file());
1118 let early_host = (!fleet_mode && spawns_worker).then(|| {
1119 let spec = superfluid_daemon::WorkerSpec {
1120 bin: runtime.worker.bin.clone(),
1121 args: runtime.serve_args(&model, ctx, max_batch, kv_bits),
1122 stderr: worker_stderr.clone(),
1123 };
1124 std::thread::spawn(move || EngineHost::spawn_process(spec))
1125 });
1126 let tokenizer = open_tokenizer(&runtime, &model).unwrap_or_else(|e| {
1127 eprintln!("superfluid: {e}");
1128 std::process::exit(2);
1129 });
1130 let codec: Box<dyn superfluid_daemon::TextCodec + Send + Sync> = match codec_for_dialect(&dialect, tokenizer) {
1131 Ok((c, kind)) => {
1132 if dialect == "auto" {
1133 match kind {
1134 "template" => eprintln!("superfluid: serving via the model's own chat template"),
1135 "raw" => eprintln!(
1136 "superfluid: model has no chat template; serving raw (chat routes will refuse)"
1137 ),
1138 _ => {}
1139 }
1140 }
1141 c
1142 }
1143 Err(e) => {
1144 eprintln!("superfluid: {e}");
1145 std::process::exit(2);
1146 }
1147 };
1148
1149 if fleet_mode {
1150 let fleet_addrs = fleet.clone().unwrap_or_default();
1151 use superfluid_daemon::fleet_manager::{FleetManager, NodeSpec, PlacementPolicy};
1152 let auth: Vec<u8> = match &fleet_auth {
1153 Some(pth) => std::fs::read(pth)
1154 .map(|t| t.trim_ascii().to_vec())
1155 .unwrap_or_else(|e| {
1156 eprintln!("superfluid: cannot read --fleet-auth {}: {e}", pth.display());
1157 std::process::exit(2);
1158 }),
1159 None => superfluid_daemon::fleet_token::read(catalog.home()).unwrap_or_default(),
1160 };
1161 if auth.is_empty() {
1162 eprintln!("superfluid: no fleet token (`superfluid fleet token` makes one): the links to the nodes are not encrypted, and only nodes on this machine may join");
1163 }
1164 let specs: Vec<NodeSpec> = fleet_addrs
1165 .split(',')
1166 .map(str::trim)
1167 .filter(|a| !a.is_empty())
1168 .map(|a| NodeSpec { addr: a.to_string(), auth: auth.clone() })
1169 .collect();
1170 if specs.is_empty() && fleet_listen.is_none() {
1171 eprintln!("superfluid: --fleet needs at least one host:port, or --fleet-listen for nodes that join");
1172 std::process::exit(2);
1173 }
1174 let policy = match fleet_policy.as_str() {
1175 "load-aware" => PlacementPolicy::LoadAware,
1176 "least-loaded" => PlacementPolicy::LeastLoaded,
1177 "round-robin" => PlacementPolicy::RoundRobin,
1178 other => {
1179 eprintln!(
1180 "superfluid: unknown --fleet-policy '{other}' \
1181 (load-aware|least-loaded|round-robin)"
1182 );
1183 std::process::exit(2);
1184 }
1185 };
1186 let Some(addr) = http else {
1187 eprintln!("superfluid: --fleet requires --http <addr> (the fleet OpenAI endpoint)");
1188 std::process::exit(2);
1189 };
1190 let lease = superfluid_proto::linkf::Lease { duration_ms: 60_000, renew_by_ms: 40_000 };
1191 let n_nodes = specs.len();
1192 let manager = Arc::new(FleetManager::with_options(
1193 specs,
1194 policy,
1195 lease,
1196 fleet_pool_high,
1197 fleet_conns,
1198 ));
1199 let mut reached = 0;
1200 for node in manager.probe() {
1201 match node {
1202 Ok(n) => {
1203 reached += 1;
1204 eprintln!(
1205 "superfluid: fleet node '{}' at {}: {}, context window {} tokens, {} lanes",
1206 n.identity,
1207 n.addr,
1208 n.models.join(", "),
1209 n.max_context_tokens,
1210 n.max_lanes
1211 );
1212 if !n.models.contains(&model_name) {
1213 eprintln!(
1214 "superfluid: fleet node '{}' serves {}, not {model_name}, so nothing is placed \
1215 there; start it with --model-name {model_name}",
1216 n.identity,
1217 n.models.join(", ")
1218 );
1219 }
1220 }
1221 Err(e) => eprintln!("superfluid: {e}; retrying it in the background"),
1222 }
1223 }
1224 if max_context.is_none() {
1225 match manager.advertised_context(&model_name) {
1226 Some(n) => eprintln!("superfluid: context window {n} tokens (the largest node's)"),
1227 None => eprintln!(
1228 "superfluid: context window unknown until a node serving {model_name} answers"
1229 ),
1230 }
1231 }
1232 manager.spawn_keepalive();
1233 let _announced = fleet_listen.as_deref().map(|listen| {
1234 let listener = std::net::TcpListener::bind(listen).unwrap_or_else(|e| {
1235 eprintln!("superfluid: --fleet-listen {listen}: {e}");
1236 std::process::exit(1);
1237 });
1238 let port = listener.local_addr().map(|a| a.port()).unwrap_or_default();
1239 eprintln!("superfluid: fleet head takes joining nodes on {listen}");
1240 let (m, a, name) = (Arc::clone(&manager), auth.clone(), model_name.clone());
1241 std::thread::spawn(move || superfluid_daemon::fleet_join::accept_nodes(listener, m, a, name));
1242 let http_port = addr.parse::<std::net::SocketAddr>().ok().filter(|a| !a.ip().is_loopback()).map(|a| a.port());
1244 match superfluid_daemon::fleet_mdns::advertise(port, &model_name, http_port) {
1245 Ok(d) => Some(d),
1246 Err(e) => {
1247 eprintln!("superfluid: not announced on the local network ({e}); give nodes this head's address");
1248 None
1249 }
1250 }
1251 });
1252 if !qos_flags.is_empty() {
1253 eprintln!(
1254 "superfluid: {} not supported in fleet-head mode (the fleet head has no \
1255 local scheduler and Link F carries no QoS class); set them on each \
1256 node's own `superfluid serve` instead",
1257 qos_flags.join(", ")
1258 );
1259 std::process::exit(2);
1260 }
1261 if op_spec_max_temperature.is_some() {
1262 eprintln!(
1263 "superfluid: --spec-max-temperature is not supported in fleet-head mode; \
1264 set it on each node's own `superfluid serve` instead"
1265 );
1266 std::process::exit(2);
1267 }
1268 if fim_model.is_some() {
1269 eprintln!(
1270 "superfluid: --fim-model is not supported in fleet-head mode (the head hosts no \
1271 engine); set it on a node's own `superfluid serve` instead"
1272 );
1273 std::process::exit(2);
1274 }
1275 if op_repeat_penalty.is_some() {
1276 eprintln!(
1277 "superfluid: --repeat-penalty is not supported in fleet-head mode \
1278 (the fleet protocol carries no per-request penalty); \
1279 set it on each node's own `superfluid serve` instead"
1280 );
1281 std::process::exit(2);
1282 }
1283 let http_listener = listen_or_exit("--http", &addr);
1284 eprintln!(
1285 "superfluid: fleet-head openai api on http://{addr} over {n_nodes} node(s), {reached} reachable"
1286 );
1287 if key_policy.is_some() {
1288 eprintln!(
1289 "superfluid: --key-policy at the fleet head: keys, rate and concurrency limits \
1290 apply; class and max_class do not (the head has no local scheduler)"
1291 );
1292 }
1293 let codec_arc: Arc<dyn superfluid_daemon::TextCodec + Send + Sync> = Arc::from(codec);
1294 superfluid_daemon::fleet_openai::serve_blocking(
1295 http_listener,
1296 manager,
1297 codec_arc,
1298 "raw".to_string(),
1299 model_name.clone(),
1300 max_context.map_or(0, |n| n.max(512) as u32),
1301 superfluid_daemon::openai::ServeConfig {
1302 api_key: api_key.clone(),
1303 rate_limit_per_minute: rate_limit,
1304 keys: key_policy.clone(),
1305 sampling: superfluid_daemon::openai::SamplingOverrides {
1306 temperature: op_temperature,
1307 top_p: op_top_p,
1308 top_k: op_top_k,
1309 min_p: op_min_p,
1310 repeat_penalty: op_repeat_penalty,
1311 spec_max_temperature: None,
1312 fallback: embedding.sampling_fallback,
1313 },
1314 ..Default::default()
1315 },
1316 )
1317 .expect("fleet serve");
1318 return;
1319 }
1320
1321 let wal_path = sessions.join("wal.log");
1322 let store = match SessionStore::open(&wal_path) {
1323 Ok(store) => store,
1324 Err(e @ (superfluid_daemon::DaemonError::WalCorrupt | superfluid_daemon::DaemonError::Codec(_))) => {
1325 eprintln!(
1326 "superfluid: the session log {} is damaged ({e}). A record with more of the log \
1327 after it fails its checksum, or a whole record does not decode, so this was not a \
1328 crash mid-write (a torn last record is dropped on its own); this server won't guess \
1329 which history to keep. Move the file aside to start fresh, or run with \
1330 --sessions-cache if sessions don't need to survive a restart.",
1331 wal_path.display()
1332 );
1333 std::process::exit(1);
1334 }
1335 Err(e) => {
1336 eprintln!("superfluid: could not open the session log {}: {e}", wal_path.display());
1337 std::process::exit(1);
1338 }
1339 };
1340
1341 let workerd_bin = catalog.workerd().to_path_buf();
1342 let worker_process = if !runtime.in_process_capable() {
1343 if !worker_process {
1344 eprintln!(
1345 "superfluid: --no-worker-process: the {} runtime runs in its own worker ({})",
1346 runtime.id.name(),
1347 runtime.worker.bin.display()
1348 );
1349 }
1350 true
1351 } else if worker_process && !runtime.worker.bin.is_file() {
1352 if fim_model.is_some() {
1353 eprintln!(
1354 "superfluid: --fim-model needs superfluid-workerd (the satellite runs in its own \
1355 worker), not found at {}",
1356 workerd_bin.display()
1357 );
1358 std::process::exit(1);
1359 }
1360 if models.len() > 1 {
1361 eprintln!(
1362 "superfluid: {} models need superfluid-workerd (one address space each), \
1363 not found at {}",
1364 models.len(),
1365 workerd_bin.display()
1366 );
1367 std::process::exit(1);
1368 }
1369 eprintln!(
1370 "superfluid: superfluid-workerd not found at {}; running the engine in-process \
1371 (install the bundled workerd or pass --no-worker-process to silence)",
1372 workerd_bin.display()
1373 );
1374 false
1375 } else {
1376 worker_process
1377 };
1378 let model_display = model.display().to_string();
1379 let model_source = model.to_string_lossy().into_owned();
1380 let host = if worker_process {
1381 match early_host {
1382 Some(started) => started.join().expect("worker start thread"),
1383 None => EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
1384 bin: runtime.worker.bin.clone(),
1385 args: runtime.serve_args(&model, ctx, max_batch, kv_bits),
1386 stderr: worker_stderr.clone(),
1387 }),
1388 }
1389 .unwrap_or_else(|e| {
1390 #[cfg(feature = "tui")]
1391 if let Some(c) = stderr_capture.as_ref() {
1392 c.drain(std::time::Duration::from_secs(2));
1393 }
1394 eprintln!(
1395 "superfluid: could not start the engine worker for '{model_display}': {e} \
1396 (the worker's stderr above carries the engine's reason)"
1397 );
1398 std::process::exit(1);
1399 })
1400 } else {
1401 superfluid_daemon::linked::spawn(runtime.id, model, ctx, max_batch, kv_bits)
1402 .unwrap_or_else(|| Err(format!("this build does not link the {} runtime", runtime.id)))
1403 .unwrap_or_else(|e| {
1404 eprintln!("superfluid: could not load '{model_display}': {e}");
1405 std::process::exit(1);
1406 })
1407 };
1408
1409 let caps = host.capabilities().read().expect("capability record").clone();
1410 if let Err(e) = caps.check_startup_flags(park_lossy, kv_bits_arg, speculate.as_deref()) {
1411 eprintln!("superfluid: {e}");
1412 std::process::exit(2);
1413 }
1414 if prefix_blob_budget_mb > 0 {
1415 if let superfluid_daemon::capabilities::Cap::No(why) = caps.get("ops", "space_promote") {
1416 eprintln!("superfluid: --prefix-blob-budget-mb has no effect on runtime {}: {why}", runtime.id.name());
1417 }
1418 }
1419
1420 let listener = socket.as_ref().map(|socket| {
1421 let _ = std::fs::remove_file(socket);
1422 let listener = UnixListener::bind(socket).unwrap_or_else(|e| {
1423 eprintln!("superfluid: cannot listen on --socket {}: {e}", socket.display());
1424 std::process::exit(1);
1425 });
1426 eprintln!("superfluid: serving on {}", socket.display());
1427 listener
1428 });
1429 let park_dir = park.then(|| sessions.join("park"));
1430
1431 let codec: Box<dyn superfluid_daemon::TextCodec + Send + Sync> = match tool_envelope {
1432 Some(env) => {
1433 if let Some(why) = superfluid_daemon::codec::tool_envelope_override_conflict(&*codec, env) {
1434 eprintln!("superfluid: {why}");
1435 std::process::exit(2);
1436 }
1437 eprintln!("superfluid: tool-call envelope overridden to '{}'", env.name());
1438 Box::new(superfluid_daemon::codec::WithToolEnvelope::new(codec, env))
1439 }
1440 None => codec,
1441 };
1442 let daemon = Arc::new(
1443 Daemon::with_options(
1444 store,
1445 host,
1446 codec,
1447 superfluid_daemon::DaemonOptions {
1448 max_lanes: max_batch as usize,
1449 park_dir,
1450 park_lossy,
1451 park_budget_bytes: park_budget_gb.saturating_mul(1 << 30),
1452 tool_lease_ms,
1453 tick_decode_budget,
1454 tick_target_ms,
1455 runtime_id: Some(runtime.id.name()),
1456 prefill_budget,
1457 agent_starvation_ticks: starvation_ticks,
1458 class_lanes,
1459 pressure_high_pct: if pressure_high > 0 { pressure_high } else { 85 },
1460 pressure_low_pct: if pressure_low > 0 { pressure_low } else { 70 },
1461 pin_budget_pct,
1462 pressure_source: if os_pressure {
1463 superfluid_daemon::pressure::PressureConfig::Os
1464 } else {
1465 superfluid_daemon::pressure::PressureConfig::None
1466 },
1467 speculate: speculate_primary.clone(),
1468 speculate_auto: speculate.as_deref().map(str::trim) == Some("auto"),
1469 speculate_fallbacks: speculate_fallbacks.clone(),
1470 speculation: speculation.clone(),
1471 media_dir: Some(sessions.join("media")),
1472 files_max_bytes,
1473 files_expiry_secs: files_expiry,
1474 completion_deadline_ms,
1475 trace_scope: superfluid_daemon::otlp::TraceScope::model(&model_name),
1476 completion_bucket,
1477 },
1478 )
1479 .unwrap_or_else(|e| {
1480 #[cfg(feature = "tui")]
1481 if let Some(c) = stderr_capture.as_ref() {
1482 c.drain(std::time::Duration::from_secs(2));
1483 }
1484 eprintln!("superfluid: cannot serve '{model_display}': {e}");
1485 std::process::exit(2);
1486 }),
1487 );
1488 {
1489 let bits = daemon.sched_stats().kv_bits.load(std::sync::atomic::Ordering::Relaxed);
1490 match superfluid_daemon::scheduler::kv_bits_label(bits) {
1491 Some(label) if kv_bits != 0 && bits != kv_bits as u64 => {
1492 eprintln!("superfluid: kv cache {label} (--kv-bits {kv_bits} overridden by the engine, see above)")
1493 }
1494 Some(label) => eprintln!("superfluid: kv cache {label}"),
1495 None => {}
1496 }
1497 }
1498 let fim_satellite: Option<(String, Arc<Daemon>)> = fim_model.as_ref().map(|path| {
1499 let runtime = catalog.reader_of(path).and_then(|id| catalog.require(id)).unwrap_or_else(|e| {
1500 eprintln!("superfluid: --fim-model {}: {e}", path.display());
1501 std::process::exit(2);
1502 });
1503 let mut fim_ctx = fim_max_context.unwrap_or(ctx.min(8192)).max(512);
1504 if max_context_auto && fim_ctx > ctx {
1505 eprintln!(
1506 "superfluid: --fim-max-context {fim_ctx} is above the window sized for this device; \
1507 the FIM satellite runs at {ctx}"
1508 );
1509 fim_ctx = ctx;
1510 }
1511 let sat = build_fim_satellite(&FimSatelliteCfg {
1512 model: path.clone(),
1513 runtime,
1514 sessions: sessions.clone(),
1515 ctx: fim_ctx,
1516 max_batch: fim_max_batch,
1517 kv_bits,
1518 pressure_high,
1519 pressure_low,
1520 os_pressure,
1521 completion_deadline_ms,
1522 worker_stderr: worker_stderr.clone(),
1523 })
1524 .unwrap_or_else(|e| {
1525 #[cfg(feature = "tui")]
1526 if let Some(c) = stderr_capture.as_ref() {
1527 c.drain(std::time::Duration::from_secs(2));
1528 }
1529 eprintln!("superfluid: could not start the --fim-model satellite '{}': {e}", path.display());
1530 std::process::exit(1);
1531 });
1532 let name = superfluid_daemon::registry::model_id_for(path);
1533 eprintln!("superfluid: FIM completions served by satellite '{name}'");
1534 (name, sat)
1535 });
1536 if let Some((name, sat)) = &fim_satellite {
1537 if let Err(e) = daemon.attach_fim_satellite(name.clone(), Arc::clone(sat)) {
1538 eprintln!(
1539 "superfluid: --fim-model {}: {e} (its tokenizer has no atomic <|fim_prefix|>/<|fim_suffix|>/<|fim_middle|> tokens)",
1540 fim_model.as_ref().map(|p| p.display().to_string()).unwrap_or_default()
1541 );
1542 std::process::exit(2);
1543 }
1544 }
1545 let state_names: StateNames = Arc::default();
1546 let registry = if worker_process {
1547 let cfg = ModelBuildCfg {
1548 model_name: embedding.model_name,
1549 base_sessions: sessions.clone(),
1550 state_names: Arc::clone(&state_names),
1551 catalog: Arc::clone(&catalog),
1552 pick: runtime_pick.clone(),
1553 dialect: dialect.clone(),
1554 ctx,
1555 max_batch,
1556 kv_bits,
1557 park,
1558 park_lossy,
1559 park_budget_gb,
1560 tool_lease_ms,
1561 tick_decode_budget,
1562 tick_target_ms,
1563 prefill_budget,
1564 starvation_ticks,
1565 class_lanes,
1566 os_pressure,
1567 pressure_high_pct: if pressure_high > 0 { pressure_high } else { 85 },
1568 pressure_low_pct: if pressure_low > 0 { pressure_low } else { 70 },
1569 pin_budget_pct,
1570 speculate: speculate.clone(),
1571 speculation: speculation.clone(),
1572 completion_deadline_ms,
1573 completion_bucket,
1574 fim_satellite: fim_satellite.clone(),
1575 worker_stderr: worker_stderr.clone(),
1576 };
1577 let reload_cfg = cfg.clone();
1578 let loader: superfluid_daemon::registry::ModelLoader =
1579 Box::new(move |id: &str, runtime| build_loaded_daemon(&cfg, std::path::Path::new(id), runtime));
1580 let registry = superfluid_daemon::registry::ModelRegistry::with_initial(model_name.clone(), Arc::clone(&daemon), loader);
1581 registry.remember_source_on(&model_name, &model_source, Some(runtime.id));
1582 let names = Arc::clone(&catalog);
1583 registry.set_runtime_names(Box::new(move |name: &str| {
1584 names.named(name).map_err(|why| format!("{why} ({})", names.here_described()))
1585 }));
1586 registry.set_reloader(Box::new(move |id: &str, runtime, retired: &Daemon| {
1587 reload_daemon(&reload_cfg, std::path::Path::new(id), runtime, retired)
1588 }));
1589 Arc::new(registry)
1590 } else {
1591 Arc::new(superfluid_daemon::registry::ModelRegistry::single(
1592 model_name.clone(),
1593 Arc::clone(&daemon),
1594 ))
1595 };
1596 {
1597 let (registry, name) = (Arc::downgrade(®istry), model_name.clone());
1598 daemon.on_retired(Box::new(move || {
1599 if let Some(registry) = registry.upgrade() {
1600 let _ = registry.resolve(Some(&name));
1601 }
1602 }));
1603 }
1604 let same = |p: &std::path::Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
1605 let mut taken: std::collections::HashSet<String> = std::collections::HashSet::from([model_name.clone()]);
1606 let mut given: std::collections::HashSet<PathBuf> = models.first().map(|m| same(m)).into_iter().collect();
1607 for extra in models.iter().skip(1) {
1608 if !given.insert(same(extra)) {
1609 eprintln!("superfluid: model '{}' already loaded, skipping", (embedding.model_name)(extra, &Default::default()));
1610 continue;
1611 }
1612 let name = match pulled_names.get(extra) {
1613 Some(name) => name.clone(),
1614 None => {
1615 let name = (embedding.model_name)(extra, &taken);
1616 if name != (embedding.model_name)(extra, &Default::default()) {
1619 eprintln!("superfluid: {} is registered as '{name}' (another model took its stem)", extra.display());
1620 state_names.lock().expect("state names").insert(extra.clone(), name.clone());
1621 }
1622 name
1623 }
1624 };
1625 taken.insert(name.clone());
1626 match registry.load_as(&name, &extra.to_string_lossy()) {
1627 Ok(true) => eprintln!("superfluid: loaded model '{name}'"),
1628 Ok(false) => eprintln!("superfluid: model '{name}' already loaded, skipping"),
1629 Err(e) => {
1630 #[cfg(feature = "tui")]
1631 if let Some(c) = stderr_capture.as_ref() {
1632 c.drain(std::time::Duration::from_secs(2));
1633 }
1634 eprintln!("superfluid: could not load '{}': {e}", extra.display());
1635 std::process::exit(1);
1636 }
1637 }
1638 }
1639 if let Some(dir) = &model_dir {
1640 let mut n = 0;
1641 let here = catalog.survey();
1642 match artifacts_under(dir, &here, MODEL_DIR_DEPTH) {
1643 Ok(paths) => {
1644 for path in paths {
1645 let refused = runtime_pick
1646 .resolve(&path, &catalog)
1647 .and_then(|rt| catalog.require(rt))
1648 .and_then(|rt| refuse_statically(&rt, park_lossy, kv_bits_arg, speculate.as_deref()));
1649 if let Err(e) = refused {
1650 eprintln!("superfluid: --model-dir: skipping {}: {e}", path.display());
1651 continue;
1652 }
1653 if !given.insert(same(&path)) {
1654 continue;
1655 }
1656 let name = (embedding.model_name)(&path, &taken);
1657 if registry.is_loaded(&name) {
1658 taken.insert(name);
1659 continue;
1660 }
1661 if name != (embedding.model_name)(&path, &Default::default()) {
1662 eprintln!(
1663 "superfluid: --model-dir: {} is registered as '{name}' (another artifact took its stem)",
1664 path.display()
1665 );
1666 state_names.lock().expect("state names").insert(path.clone(), name.clone());
1667 }
1668 registry.register_known(&name, &path);
1669 taken.insert(name);
1670 n += 1;
1671 }
1672 }
1673 Err(e) => {
1674 eprintln!("superfluid: --model-dir {}: {e}", dir.display());
1675 std::process::exit(1);
1676 }
1677 }
1678 eprintln!("superfluid: {n} model(s) known from {} (loaded on first use)", dir.display());
1679 }
1680 if idle_timeout > 0 {
1681 registry.track_idle();
1682 let reg = Arc::clone(®istry);
1683 let max_idle = std::time::Duration::from_secs(idle_timeout);
1684 let tick = std::time::Duration::from_secs((idle_timeout / 4).max(1));
1685 std::thread::spawn(move || loop {
1686 std::thread::sleep(tick);
1687 for name in reg.sweep_idle(max_idle) {
1688 eprintln!("superfluid: unloaded idle model '{name}'");
1689 }
1690 });
1691 }
1692 if files_expiry.is_some() && files_sweep > 0 {
1693 let d = Arc::clone(&daemon);
1694 std::thread::spawn(move || loop {
1695 std::thread::sleep(std::time::Duration::from_secs(files_sweep));
1696 let n = d.files().sweep();
1697 if n > 0 {
1698 eprintln!("superfluid: files sweep removed {n} expired file(s)");
1699 }
1700 });
1701 }
1702 let mut http_thread = None;
1703 if let Some(addr) = http {
1704 let http_listener = listen_or_exit("--http", &addr);
1705 eprintln!("superfluid: openai api on http://{addr}{}", embedding.http.also_served());
1706 superfluid_daemon::selfupdate::check_in_background(catalog.home().to_path_buf(), pull_flags.offline);
1707 if nonstream_keepalive > 0 {
1708 eprintln!(
1709 "superfluid: non-streaming keep-alive on: 200 at admission, then a space every \
1710 {nonstream_keepalive}s until the JSON body (x-superfluid-keepalive: whitespace)"
1711 );
1712 }
1713 let reg = Arc::clone(®istry);
1714 let serve_cfg = superfluid_daemon::openai::ServeConfig {
1715 api_key: api_key.clone(),
1716 rate_limit_per_minute: rate_limit,
1717 drain_timeout_secs: drain_timeout,
1718 default_max_tokens,
1719 max_context: ctx as u32,
1720 sampling: superfluid_daemon::openai::SamplingOverrides {
1721 temperature: op_temperature,
1722 top_p: op_top_p,
1723 top_k: op_top_k,
1724 min_p: op_min_p,
1725 repeat_penalty: op_repeat_penalty,
1726 spec_max_temperature: op_spec_max_temperature,
1727 fallback: embedding.sampling_fallback,
1728 },
1729 qos: superfluid_daemon::openai::HttpQosPolicy {
1730 default: http_default_qos,
1731 honor_headers: http_qos_header,
1732 allow_batch_invariant: http_allow_batch_invariant,
1733 },
1734 keys: key_policy.clone(),
1735 nonstream_keepalive_secs: nonstream_keepalive,
1736 surface: embedding.http,
1737 model_naming: embedding.model_naming,
1738 identity: embedding.identity.clone(),
1739 };
1740 http_thread = Some(std::thread::spawn(move || {
1741 superfluid_daemon::openai::serve_blocking_config(http_listener, reg, serve_cfg)
1742 }));
1743 }
1744 if let Some(addr) = web {
1745 if !addr.starts_with("127.0.0.1:") && !addr.starts_with("[::1]:") && !addr.starts_with("localhost:") {
1746 eprintln!("superfluid: --web must bind loopback (127.0.0.1/::1)");
1747 std::process::exit(2);
1748 }
1749 let token_path = sessions.join("web-token");
1750 let token = superfluid_daemon::apiweb::mint_token(&token_path).expect("mint web token");
1751 let web_listener = listen_or_exit("--web", &addr);
1752 eprintln!("superfluid: api-web on http://{addr} (token: {})", token_path.display());
1753 let d = Arc::clone(&daemon);
1754 std::thread::spawn(move || {
1755 let _ = superfluid_daemon::apiweb::serve_blocking(web_listener, d, token, web_origins);
1756 });
1757 }
1758 let _otlp_pusher = otlp_metrics.map(|url| {
1759 eprintln!(
1760 "superfluid: pushing metrics to {url}/v1/metrics every {otlp_interval_ms}ms (an EGRESS)"
1761 );
1762 superfluid_daemon::export::MetricsPusher::start(
1763 url,
1764 std::time::Duration::from_millis(otlp_interval_ms),
1765 daemon.sched_stats(),
1766 )
1767 });
1768 #[cfg(feature = "tui")]
1769 if let Some(ring) = tui_ring {
1770 if let Some(listener) = listener {
1771 let serve_daemon = Arc::clone(&daemon);
1772 std::thread::spawn(move || {
1773 let _ = api::serve(listener, serve_daemon);
1774 });
1775 }
1776 let node = superfluid_daemon::tui::NodeInfo {
1777 identity: hostname(),
1778 model: model_name.clone(),
1779 engine: if worker_process { "native (worker)".into() } else { "native".into() },
1780 max_lanes: max_batch,
1781 };
1782 superfluid_daemon::tui::run(daemon, ring, node, stderr_capture.as_ref()).expect("tui");
1783 drop(stderr_capture);
1784 return;
1785 }
1786 let Some(listener) = listener else {
1787 match http_thread.map(std::thread::JoinHandle::join) {
1790 Some(Ok(Ok(()))) => return,
1791 Some(Ok(Err(e))) => eprintln!("superfluid: the HTTP API stopped: {e}"),
1792 Some(Err(_)) => eprintln!("superfluid: the HTTP API stopped: its thread panicked"),
1793 None => unreachable!("without a session socket --no-http is refused"),
1794 }
1795 std::process::exit(1);
1796 };
1797 api::serve(listener, daemon).expect("serve");
1798}
1799
1800#[cfg(feature = "tui")]
1801fn hostname() -> String {
1802 let mut buf = [0u8; 256];
1803 let rc = unsafe { libc::gethostname(buf.as_mut_ptr().cast::<libc::c_char>(), buf.len()) };
1807 if rc == 0 {
1808 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1809 String::from_utf8_lossy(&buf[..end]).into_owned()
1810 } else {
1811 "local".into()
1812 }
1813}
1814
1815type StateNames = Arc<std::sync::Mutex<std::collections::HashMap<PathBuf, String>>>;
1816
1817#[derive(Clone)]
1818struct ModelBuildCfg {
1819 model_name: fn(&Path, &std::collections::HashSet<String>) -> String,
1820 base_sessions: std::path::PathBuf,
1821 state_names: StateNames,
1822 catalog: Arc<superfluid_daemon::runtimes::Catalog>,
1823 pick: RuntimePick,
1824 dialect: String,
1825 ctx: i32,
1826 max_batch: u32,
1827 kv_bits: i32,
1828 park: bool,
1829 park_lossy: bool,
1830 park_budget_gb: u64,
1831 tool_lease_ms: u64,
1832 tick_decode_budget: u32,
1833 tick_target_ms: u32,
1834 prefill_budget: u32,
1835 starvation_ticks: u32,
1836 class_lanes: [usize; 4],
1837 os_pressure: bool,
1838 pressure_high_pct: u8,
1839 pressure_low_pct: u8,
1840 pin_budget_pct: u8,
1841 speculate: Option<String>,
1842 speculation: superfluid_daemon::SpeculationOptions,
1843 completion_deadline_ms: u64,
1844 completion_bucket: superfluid_daemon::completion_bucket::BucketConfig,
1845 fim_satellite: Option<(String, Arc<Daemon>)>,
1846 worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>>,
1847}
1848
1849struct FimSatelliteCfg {
1850 model: PathBuf,
1851 runtime: superfluid_daemon::runtimes::Runtime,
1852 sessions: PathBuf,
1853 ctx: i32,
1854 max_batch: u32,
1855 kv_bits: i32,
1856 pressure_high: u8,
1857 pressure_low: u8,
1858 os_pressure: bool,
1859 completion_deadline_ms: u64,
1860 worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>>,
1861}
1862
1863fn build_fim_satellite(cfg: &FimSatelliteCfg) -> Result<Arc<Daemon>, superfluid_daemon::DaemonError> {
1864 use superfluid_daemon::DaemonError;
1865 let tok = open_tokenizer(&cfg.runtime, &cfg.model)
1866 .map_err(|e| DaemonError::Generation(format!("satellite tokenizer load failed: {e}")))?;
1867 let codec = superfluid_daemon::BundleCodec::from_tokenizer(tok);
1868 let host = EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
1869 bin: cfg.runtime.worker.bin.clone(),
1870 args: cfg.runtime.serve_args(&cfg.model, cfg.ctx, cfg.max_batch, cfg.kv_bits),
1871 stderr: cfg.worker_stderr.clone(),
1872 })?;
1873 let daemon = Daemon::with_options(
1874 SessionStore::ephemeral(),
1875 host,
1876 Box::new(codec),
1877 superfluid_daemon::DaemonOptions {
1878 max_lanes: cfg.max_batch as usize,
1879 park_dir: None,
1880 pressure_high_pct: if cfg.pressure_high > 0 { cfg.pressure_high } else { 85 },
1881 pressure_low_pct: if cfg.pressure_low > 0 { cfg.pressure_low } else { 70 },
1882 pressure_source: if cfg.os_pressure {
1883 superfluid_daemon::pressure::PressureConfig::Os
1884 } else {
1885 superfluid_daemon::pressure::PressureConfig::None
1886 },
1887 media_dir: Some(cfg.sessions.join("fim").join("media")),
1888 completion_deadline_ms: cfg.completion_deadline_ms,
1889 trace_scope: superfluid_daemon::otlp::TraceScope::model(
1890 &superfluid_daemon::registry::model_id_for(&cfg.model),
1891 ),
1892 ..Default::default()
1893 },
1894 )?;
1895 Ok(Arc::new(daemon))
1896}
1897
1898fn build_codec_for(
1899 dialect: &str,
1900 runtime: &superfluid_daemon::runtimes::Runtime,
1901 model: &std::path::Path,
1902) -> Result<Box<dyn superfluid_daemon::TextCodec + Send + Sync>, superfluid_daemon::DaemonError> {
1903 use superfluid_daemon::DaemonError;
1904 let tok = open_tokenizer(runtime, model).map_err(DaemonError::Generation)?;
1905 let (codec, _) = codec_for_dialect(dialect, tok).map_err(DaemonError::Generation)?;
1906 Ok(codec)
1907}
1908
1909fn refuse_statically(
1910 runtime: &superfluid_daemon::runtimes::Runtime,
1911 park_lossy: bool,
1912 kv_bits: Option<i32>,
1913 speculate: Option<&str>,
1914) -> Result<(), String> {
1915 runtime.static_capabilities().check_startup_flags(park_lossy, kv_bits, speculate)
1916}
1917
1918fn describe_worker(runtime: &superfluid_daemon::runtimes::Runtime) -> String {
1919 let version = runtime.version.as_deref().map(|v| format!("{v}, ")).unwrap_or_default();
1920 format!("{version}{}: {}", runtime.worker.source.name(), runtime.worker.bin.display())
1921}
1922
1923fn open_tokenizer(
1924 runtime: &superfluid_daemon::runtimes::Runtime,
1925 model: &std::path::Path,
1926) -> Result<Arc<dyn superfluid_engine::Tokenizer>, String> {
1927 use superfluid_engine::artifact::TokenizerSource;
1928 if let Some(why) = not_there(model) {
1929 return Err(why);
1930 }
1931 if let Some(lib) = &runtime.tokenizer {
1932 let tok = superfluid_daemon::dylib_tokenizer::DylibTokenizer::open(lib, model)?;
1933 return Ok(Arc::new(tok));
1934 }
1935 if runtime.info.tokenizer == Some(TokenizerSource::HuggingFace) {
1936 let tok = superfluid_tokenizer_hf::HfTokenizer::load(model)
1937 .map_err(|e| format!("tokenizer load failed for {}: {e}", model.display()))?;
1938 return Ok(Arc::new(tok));
1939 }
1940 superfluid_daemon::linked::tokenizer(runtime.id, model).unwrap_or_else(|| {
1941 Err(format!(
1942 "no tokenizer for {} here: the {} runtime ships none beside its worker, and this superfluid does not link it",
1943 model.display(),
1944 runtime.id
1945 ))
1946 })
1947}
1948
1949const MODEL_DIR_DEPTH: usize = 8;
1952
1953fn artifacts_under(
1957 dir: &std::path::Path,
1958 here: &[superfluid_daemon::runtimes::Found],
1959 depth: usize,
1960) -> std::io::Result<Vec<PathBuf>> {
1961 let mut paths: Vec<PathBuf> = std::fs::read_dir(dir)?.flatten().map(|e| e.path()).collect();
1962 paths.sort();
1963 let mut found = Vec::new();
1964 for path in paths {
1965 if !superfluid_daemon::runtimes::readers_in(here, &path).is_empty() {
1966 found.push(path);
1967 } else if depth > 0 && path.is_dir() {
1968 found.extend(artifacts_under(&path, here, depth - 1).unwrap_or_default());
1969 }
1970 }
1971 Ok(found)
1972}
1973
1974fn not_there(model: &std::path::Path) -> Option<String> {
1975 (!model.exists()).then(|| format!("{}: no such file or directory", model.display()))
1976}
1977
1978fn codec_for_dialect(
1979 dialect: &str,
1980 tok: Arc<dyn superfluid_engine::Tokenizer>,
1981) -> Result<(Box<dyn superfluid_daemon::TextCodec + Send + Sync>, &'static str), String> {
1982 Ok(match dialect {
1983 "chatml" => (
1984 Box::new(
1985 superfluid_daemon::codec::chatml_codec_from_tokenizer(tok)
1986 .map_err(|e| format!("--dialect chatml refused: {e}"))?,
1987 ),
1988 "chatml",
1989 ),
1990 "atem" => (
1991 Box::new(
1992 superfluid_daemon::atem::AtemCodec::from_tokenizer(tok)
1993 .map_err(|_| "--dialect atem refused: the tokenizer lacks the ATEM markers".to_string())?,
1994 ),
1995 "atem",
1996 ),
1997 "raw" => (Box::new(superfluid_daemon::BundleCodec::from_tokenizer(tok)), "raw"),
1998 "template" => (
1999 Box::new(
2000 superfluid_daemon::template_codec::TemplateCodec::from_tokenizer(tok)
2001 .map_err(|_| "--dialect template refused: the model carries no usable chat template".to_string())?,
2002 ),
2003 "template",
2004 ),
2005 "auto" => superfluid_daemon::codec::auto_codec_from_tokenizer(tok),
2006 other => return Err(format!("unknown dialect '{other}' (chatml|atem|raw|template|auto)")),
2007 })
2008}
2009
2010fn speculation_for<'a>(
2011 directive: Option<&'a str>,
2012 caps: &superfluid_daemon::capabilities::Capabilities,
2013 model: &std::path::Path,
2014 runtime: &str,
2015) -> Option<&'a str> {
2016 let d = directive?;
2017 if d.trim() != "auto" {
2018 return Some(d);
2019 }
2020 match caps.declines_auto_speculation() {
2021 Some(why) => {
2022 eprintln!("superfluid: speculation: auto: {} on the {runtime} runtime verifies no drafts ({why}) — decoding plainly", model.display());
2023 None
2024 }
2025 None if runtime != "basert" => {
2028 eprintln!("superfluid: speculation: auto: prompt-lookup on the {runtime} runtime");
2029 Some("prompt-lookup")
2030 }
2031 None => Some(d),
2032 }
2033}
2034
2035fn unsupported_runtime(param: Option<&'static str>) -> impl Fn(String) -> superfluid_daemon::DaemonError {
2036 move |m: String| superfluid_daemon::DaemonError::Unsupported(superfluid_daemon::Refusal::new("unsupported_runtime", param, m))
2037}
2038
2039fn runtime_for(
2040 cfg: &ModelBuildCfg,
2041 model_path: &std::path::Path,
2042 requested: Option<RuntimeId>,
2043) -> Result<superfluid_daemon::runtimes::Runtime, superfluid_daemon::DaemonError> {
2044 let runtime = cfg
2045 .pick
2046 .resolve_with(model_path, requested, &cfg.catalog)
2047 .and_then(|rt| cfg.catalog.require(rt))
2048 .map_err(unsupported_runtime(Some(if requested.is_some() { "runtime" } else { "model" })))?;
2049 refuse_statically(&runtime, cfg.park_lossy, Some(cfg.kv_bits), cfg.speculate.as_deref()).map_err(unsupported_runtime(None))?;
2050 Ok(runtime)
2051}
2052
2053fn engine_on(
2054 cfg: &ModelBuildCfg,
2055 runtime: &superfluid_daemon::runtimes::Runtime,
2056 model_path: &std::path::Path,
2057) -> Result<(Box<dyn superfluid_daemon::TextCodec + Send + Sync>, EngineHost), superfluid_daemon::DaemonError> {
2058 use superfluid_daemon::DaemonError;
2059 let codec = build_codec_for(&cfg.dialect, runtime, model_path)?;
2060 let host = EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
2061 bin: runtime.worker.bin.clone(),
2062 args: runtime.serve_args(model_path, cfg.ctx.max(512), cfg.max_batch, cfg.kv_bits),
2063 stderr: cfg.worker_stderr.clone(),
2064 })
2065 .map_err(|_| DaemonError::Generation("engine worker spawn failed".to_string()))?;
2066 host.capabilities()
2067 .read()
2068 .expect("capability record")
2069 .check_startup_flags(cfg.park_lossy, Some(cfg.kv_bits), cfg.speculate.as_deref())
2070 .map_err(unsupported_runtime(None))?;
2071 Ok((codec, host))
2072}
2073
2074fn reload_daemon(
2075 cfg: &ModelBuildCfg,
2076 model_path: &std::path::Path,
2077 requested: Option<RuntimeId>,
2078 retired: &Daemon,
2079) -> Result<(), superfluid_daemon::DaemonError> {
2080 retired.reload_with(|| {
2081 let runtime = runtime_for(cfg, model_path, requested)?;
2082 let (codec, host) = engine_on(cfg, &runtime, model_path)?;
2083 Ok((host, codec))
2084 })
2085}
2086
2087fn build_loaded_daemon(
2088 cfg: &ModelBuildCfg,
2089 model_path: &std::path::Path,
2090 requested: Option<RuntimeId>,
2091) -> Result<Arc<Daemon>, superfluid_daemon::DaemonError> {
2092 use superfluid_daemon::DaemonError;
2093 let id: String = cfg
2094 .state_names
2095 .lock()
2096 .expect("state names")
2097 .get(model_path)
2098 .cloned()
2099 .unwrap_or_else(|| (cfg.model_name)(model_path, &Default::default()));
2100 let mdir = superfluid_daemon::registry::state_dir(&cfg.base_sessions.join("models"), &id)?;
2101 let runtime = runtime_for(cfg, model_path, requested)?;
2102 settle_park_dir(&mdir.join("park"), cfg.park, cfg.park_budget_gb);
2103 let store = SessionStore::open(&mdir.join("wal.log"))?;
2104 let (codec, host) = engine_on(cfg, &runtime, model_path)?;
2105 let park_dir = cfg.park.then(|| mdir.join("park"));
2106 let caps = host.capabilities().read().expect("capability record").clone();
2107 let mut speculate_ranked = match speculation_for(cfg.speculate.as_deref(), &caps, model_path, runtime.id.name()) {
2108 Some(d) => superfluid_daemon::speculate_resolve::resolve_all(d, model_path).map_err(DaemonError::Generation)?,
2109 None => Vec::new(),
2110 };
2111 let speculate_first = (!speculate_ranked.is_empty()).then(|| speculate_ranked.remove(0));
2112 let daemon = Daemon::with_options(
2113 store,
2114 host,
2115 codec,
2116 superfluid_daemon::DaemonOptions {
2117 max_lanes: cfg.max_batch as usize,
2118 park_dir,
2119 park_lossy: cfg.park_lossy,
2120 park_budget_bytes: cfg.park_budget_gb.saturating_mul(1 << 30),
2121 tool_lease_ms: cfg.tool_lease_ms,
2122 tick_decode_budget: cfg.tick_decode_budget,
2123 tick_target_ms: cfg.tick_target_ms,
2124 prefill_budget: cfg.prefill_budget,
2125 agent_starvation_ticks: cfg.starvation_ticks,
2126 class_lanes: cfg.class_lanes,
2127 pressure_high_pct: cfg.pressure_high_pct,
2128 pressure_low_pct: cfg.pressure_low_pct,
2129 pin_budget_pct: cfg.pin_budget_pct,
2130 pressure_source: if cfg.os_pressure {
2131 superfluid_daemon::pressure::PressureConfig::Os
2132 } else {
2133 superfluid_daemon::pressure::PressureConfig::None
2134 },
2135 speculate: speculate_first,
2136 speculate_auto: cfg.speculate.as_deref().map(str::trim) == Some("auto"),
2137 speculate_fallbacks: speculate_ranked,
2138 speculation: cfg.speculation.clone(),
2139 media_dir: Some(mdir.join("media")),
2140 completion_deadline_ms: cfg.completion_deadline_ms,
2141 trace_scope: superfluid_daemon::otlp::TraceScope::model(&id),
2142 completion_bucket: cfg.completion_bucket,
2143 runtime_id: Some(runtime.id.name()),
2144 ..Default::default()
2145 },
2146 )?;
2147 if let Some((name, sat)) = &cfg.fim_satellite {
2148 daemon.attach_fim_satellite(name.clone(), Arc::clone(sat))?;
2149 }
2150 Ok(Arc::new(daemon))
2151}
2152
2153fn lock_sessions_dir(sessions: &std::path::Path) -> std::fs::File {
2154 use std::os::fd::AsRawFd;
2155 let path = sessions.join("serve.lock");
2156 let file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&path) {
2157 Ok(file) => file,
2158 Err(e) => {
2159 eprintln!("superfluid: could not open {}: {e}", path.display());
2160 std::process::exit(1);
2161 }
2162 };
2163 loop {
2164 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
2166 if rc == 0 {
2167 return file;
2168 }
2169 let err = std::io::Error::last_os_error();
2170 match err.kind() {
2171 std::io::ErrorKind::Interrupted => continue,
2172 std::io::ErrorKind::WouldBlock => {
2173 eprintln!(
2174 "superfluid: another server is already using the sessions directory {}. \
2175 Stop it, or give this one its own --sessions directory.",
2176 sessions.display()
2177 );
2178 std::process::exit(1);
2179 }
2180 _ => {
2181 eprintln!("superfluid: could not lock {}: {err}", path.display());
2182 std::process::exit(1);
2183 }
2184 }
2185 }
2186}
2187
2188fn rotate_session_wal(dir: &std::path::Path) {
2189 use superfluid_daemon::store_id::store_id_path;
2190 let wal = dir.join("wal.log");
2191 let prev = dir.join("wal.log.prev");
2192 let keep = std::fs::metadata(&wal).is_ok_and(|m| m.len() > 0);
2193 let aside = |from: &std::path::Path, to: Option<&std::path::Path>| -> bool {
2194 if !from.exists() {
2195 return false;
2196 }
2197 if to.is_some_and(|to| std::fs::rename(from, to).is_ok()) {
2198 return true;
2199 }
2200 if let Err(e) = std::fs::remove_file(from) {
2201 eprintln!(
2202 "superfluid: --sessions-cache could not set aside or remove {} ({e}); \
2203 remove it, or run without --sessions-cache.",
2204 from.display()
2205 );
2206 std::process::exit(1);
2207 }
2208 false
2209 };
2210 if keep {
2211 let _ = std::fs::remove_file(store_id_path(&prev));
2212 }
2213 if keep && aside(&wal, Some(&prev)) {
2214 aside(&store_id_path(&wal), Some(&store_id_path(&prev)));
2215 } else {
2216 aside(&wal, None);
2217 aside(&store_id_path(&wal), None);
2218 }
2219}
2220#[cfg(test)]
2221mod tests {
2222 use super::*;
2223 use superfluid_daemon::runtimes::{Found, Info};
2224 use superfluid_engine::artifact::Format;
2225
2226 fn reads(id: &'static str, format: Format) -> Found {
2227 let info = Info { aliases: Vec::new(), formats: vec![format], tokenizer: None, named: None };
2228 Found { id: RuntimeId::new(id), worker: None, info: Some(info), usable: Err("not started".into()) }
2229 }
2230
2231 #[test]
2232 fn model_dir_looks_into_a_hub_cache_and_takes_a_directory_model_whole() {
2233 let root = std::env::temp_dir().join(format!("superfluid-model-dir-{}", std::process::id()));
2234 let _ = std::fs::remove_dir_all(&root);
2235 let file = |rel: &str| {
2236 let p = root.join(rel);
2237 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
2238 std::fs::write(&p, b"BASE....").unwrap();
2239 p
2240 };
2241 let flat = file("Flat-Q4.base");
2242 let q4 = file("org/Model/default-q4/model.base");
2243 file("org/Model/default-q4/hub.json");
2244 let q8 = file("org/Model/default-q8/model.base");
2245 file("notes.txt");
2246 file("mlx-model/config.json");
2247 file("mlx-model/weights.safetensors");
2248 file("mlx-model/stray.base");
2249 let here = [
2250 reads("basert", Format::file("base", "a .base bundle", &["base"], None)),
2251 reads("mlx", Format::directory("mlx", "an MLX model", &["config.json"], &["safetensors"])),
2252 ];
2253 let found = artifacts_under(&root, &here, MODEL_DIR_DEPTH).unwrap();
2254 assert_eq!(found, vec![flat, root.join("mlx-model"), q4, q8]);
2255 assert!(artifacts_under(&root, &here, 0).unwrap().len() == 2, "depth 0 reads only the top level");
2256 }
2257}