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