Skip to main content

superfluid_daemon/
serve.rs

1//! `superfluid serve`: the server, as a library function so that another
2//! program can run it.
3
4use 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
101/// The session log under `sessions`, or the reason it cannot be served from, and exit.
102fn 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/// What a program running [`run`] in place of `superfluid serve` changes.
138/// [`Embedding::default`] is `superfluid serve` itself.
139#[derive(Debug, Clone)]
140pub struct Embedding {
141    /// The APIs served beside the OpenAI-compatible routes.
142    pub http: superfluid_daemon::openai::HttpSurface,
143    /// How a request's `model` picks the model that serves it.
144    pub model_naming: superfluid_daemon::openai::ModelNaming,
145    /// Names registered artifacts (initial, additional and model-dir models).
146    /// Receives already registered names so an embedding can disambiguate
147    /// variants. Explicit HTTP model IDs are not rewritten.
148    pub model_name: fn(&Path, &std::collections::HashSet<String>) -> String,
149    /// Sampling policy for omitted parameters; None is superfluid's default.
150    pub sampling_fallback: Option<superfluid_daemon::openai::SamplingFallback>,
151    /// The name and version the HTTP API reports for the server.
152    pub identity: superfluid_daemon::openai::ServerIdentity,
153    /// Whether the native session API listens on a unix socket (`--socket`).
154    /// Without it the HTTP API is all there is, and `--no-http` is refused.
155    pub session_socket: bool,
156    /// The program that runs a linked runtime's worker, in place of the `superfluid-workerd`
157    /// beside this executable; it passes [`crate::workerd::main`]'s arguments to it.
158    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
175/// Runs `superfluid serve`; `args` is its command line after `serve`.
176pub 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    // A runtime's option, kept as given and resolved once the catalog says which runtimes are here.
255    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        // SERVE-FLAGS-BEGIN: every flag `serve` reads is listed in superfluid_daemon::cli (a test checks).
261        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        // SERVE-FLAGS-END
570    }
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    // Set before the runtime is required, so an option that makes it usable here counts for the first probe.
596    superfluid_daemon::runtime_options::set(superfluid_daemon::runtime_options::given_pairs(&runtime_options));
597    // The llama.cpp tokenizer this process loads finds its library the way the worker does.
598    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        // A fleet head takes a model given by id from a node serving it, weights and all left there.
619        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        // The runtime first, so one missing exits as it would for a model given by path (4).
630        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        // Every model the head serves: one given by path is opened here for its tokenizer and
756        // chat template only; one given by id takes them from a node serving it, below.
757        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            // `superfluid top` finds the head's API by this; one on loopback is no use off this machine.
836            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        // A model given by id is served once a node serving it has shared its tokenizer.
887        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            // The first model's sessions where a single server keeps them; each other's in a
918            // directory of its own, as a server with several models keeps them.
919            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 &registry {
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                // The runtime answers without its options and refuses one of them: say which. Asked once, since
993                // asking starts a worker.
994                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                // Not here, or cannot run here: exit 4, so a host can tell it from a flag it got wrong;
1001                // an install tried and failed is 1, a failure along the way (`pulls::Unready`).
1002                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    // Each runtime's options are checked against what its worker declares before any model loads.
1011    superfluid_daemon::runtime_options::settle(&runtime_options, &runtime, &catalog).unwrap_or_else(|e| refuse(2, e));
1012    // A drafter given by id is pulled as a model given by id is, by the runtime that drafts for it, before
1013    // anything sizes or loads.
1014    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 directive = speculation_for(speculate.as_deref(), &runtime.static_capabilities(), &model, runtime.id.name());
1026    let resolved = directive.map(|d| {
1027        let pull = auto_drafter_pull(&catalog, runtime.id, &model);
1028        let pull = (!pull_flags.offline).then_some(&pull as &dyn Fn(&str));
1029        superfluid_daemon::speculate_resolve::resolve_all(d, &model, pull)
1030    });
1031    let (speculate_primary, speculate_fallbacks): (Option<String>, Vec<String>) = match resolved {
1032        None => (None, Vec::new()),
1033        Some(Ok(mut ranked)) => {
1034            if let Some(r) = ranked.first() {
1035                eprintln!("superfluid: speculation: {r}");
1036            } else {
1037                let dirs = superfluid_daemon::speculate_resolve::auto_search_dirs(&model);
1038                let dirs: Vec<_> = dirs.iter().map(|(d, _)| d.display().to_string()).collect();
1039                eprintln!(
1040                    "superfluid: speculation: auto found no head in {} and no fitting drafter in {} — decoding plainly",
1041                    model.display(),
1042                    dirs.join(", ")
1043                );
1044            }
1045            let primary = (!ranked.is_empty()).then(|| ranked.remove(0));
1046            (primary, ranked)
1047        }
1048        Some(Err(e)) => {
1049            eprintln!("superfluid: {e}");
1050            std::process::exit(2);
1051        }
1052    };
1053    if op_spec_max_temperature.is_some() && speculate.is_none() {
1054        eprintln!("superfluid: --spec-max-temperature ignored: no --speculate strategy is configured");
1055    }
1056    if tui_flag && !cfg!(feature = "tui") {
1057        eprintln!(
1058            "superfluid: --tui: this build carries no TUI (build superfluid-daemon with the `tui` feature, \
1059             which `basert` includes)"
1060        );
1061        std::process::exit(2);
1062    }
1063    let class_lanes = match class_lanes_spec.as_deref() {
1064        None => [0; 4],
1065        Some(spec) => superfluid_daemon::qos::parse_class_lanes(spec, max_batch as usize).unwrap_or_else(|e| {
1066            eprintln!("superfluid: --class-lanes: {e}");
1067            std::process::exit(2);
1068        }),
1069    };
1070    if let Err(e) = completion_bucket.validate() {
1071        eprintln!("superfluid: --completion-rate/--completion-burst: {e}");
1072        std::process::exit(2);
1073    }
1074    if fim_max_batch == 0 {
1075        eprintln!("superfluid: --fim-max-batch must be at least 1");
1076        std::process::exit(2);
1077    }
1078    if sessions_cache && park {
1079        eprintln!(
1080            "superfluid: --sessions-cache discards the session log at each start, which \
1081             --park{} needs to resume parked sessions. Use one or the other.",
1082            if park_lossy { "-lossy" } else { "" }
1083        );
1084        std::process::exit(2);
1085    }
1086    if models.len() > 1 && !worker_process {
1087        eprintln!("superfluid: {} models — enabling --worker-process (each needs its own address space)", models.len());
1088        worker_process = true;
1089    }
1090    if fim_model.is_some() && !worker_process {
1091        eprintln!("superfluid: --fim-model needs its own worker — enabling --worker-process");
1092        worker_process = true;
1093    }
1094    if model_dir.is_some() && !worker_process {
1095        eprintln!("superfluid: --model-dir needs a per-model worker — enabling --worker-process");
1096        worker_process = true;
1097    }
1098    if let Some(path) = &log_file {
1099        use std::os::fd::AsRawFd;
1100        match std::fs::OpenOptions::new().create(true).append(true).open(path) {
1101            Ok(f) => {
1102                // SAFETY: `f` is an open, valid fd for the duration of the
1103                // call; dup2 onto STDERR_FILENO is the documented way to
1104                // redirect a stream. The original stderr is closed by dup2.
1105                unsafe {
1106                    libc::dup2(f.as_raw_fd(), libc::STDERR_FILENO);
1107                }
1108                drop(f);
1109            }
1110            Err(e) => {
1111                eprintln!("superfluid: could not open --log-file {}: {e}", path.display());
1112                std::process::exit(1);
1113            }
1114        }
1115    }
1116    if api_key.is_none() && key_policy.is_none() {
1117        let loopback = |addr: &str| {
1118            addr.starts_with("localhost:") || addr.parse::<std::net::SocketAddr>().is_ok_and(|a| a.ip().is_loopback())
1119        };
1120        if http.as_deref().is_none_or(loopback) && web.as_deref().is_none_or(loopback) {
1121            eprintln!("superfluid: no --api-key: the HTTP API is open to every process on this machine");
1122        } else {
1123            eprintln!(
1124                "superfluid: WARNING --api-key not set. Authentication is OFF — anyone \
1125                 who can reach this port can use the API. Bind loopback or set --api-key."
1126            );
1127        }
1128    }
1129    if park && !fleet_mode {
1130        let budget = if park_budget_gb == 0 {
1131            "NO budget: --park-budget-gb 0 disables the retention sweep, so the \
1132             directory grows without limit"
1133                .to_string()
1134        } else {
1135            format!("budget {park_budget_gb} GiB, oldest evicted first")
1136        };
1137        eprintln!(
1138            "superfluid: WARNING --park is on. Every finishing DURABLE session copies its \
1139             whole KV off the GPU on the tick thread before it retires — seconds and \
1140             gigabytes at agent context, and every other lane waits — then keeps an \
1141             artifact under <sessions>/park ({budget}). A /v1/* request mints a fresh \
1142             session and can never resume it, so its TERMINAL park is skipped; one \
1143             PREEMPTED under load still parks, because it is requeued under the same id \
1144             and adopts that artifact. This pays off only for sessions you resume by id \
1145             through the session API."
1146        );
1147    }
1148
1149    let model_name = pulled_names.get(&model).cloned().unwrap_or_else(|| (embedding.model_name)(&model, &Default::default()));
1150    if prefix_blob_budget_mb > 0 {
1151        std::env::set_var("BASERT_PREFIX_BLOB_BUDGET_MB", prefix_blob_budget_mb.to_string());
1152    }
1153    if std::env::var_os("BASERT_YARN").is_some() {
1154        eprintln!(
1155            "superfluid: WARNING: BASERT_YARN is set but has no effect on this path — the rope \
1156             kernels disable YaRN under paged/batched dispatch, which serving always uses. \
1157             Long requests run UNSCALED past the trained window."
1158        );
1159    }
1160
1161    let tui_on =
1162        cfg!(feature = "tui") && !no_tui && (tui_flag || std::io::IsTerminal::is_terminal(&std::io::stdout()));
1163    let tui_ring = if tui_on {
1164        Some(superfluid_daemon::telemetry::new_log_ring())
1165    } else {
1166        None
1167    };
1168    #[cfg(feature = "tui")]
1169    let stderr_capture = tui_ring.as_ref().and_then(|ring| {
1170        match superfluid_daemon::tui::capture_stderr(ring.clone()) {
1171            Ok(guard) => Some(guard),
1172            Err(e) => {
1173                eprintln!("superfluid: stderr capture unavailable, engine output may overdraw the TUI: {e}");
1174                None
1175            }
1176        }
1177    });
1178    #[cfg(feature = "tui")]
1179    let worker_stderr = stderr_capture.as_ref().map(|c| c.worker_stderr());
1180    #[cfg(not(feature = "tui"))]
1181    let worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>> = None;
1182    let log_dir_eff = log_dir.or(if tui_on { Some(sessions.join("logs")) } else { None });
1183    if let Some(url) = &otlp_metrics {
1184        if let Err(e) = superfluid_daemon::otlp::validate_http_url(url) {
1185            eprintln!("superfluid: --otlp-metrics: {e}");
1186            std::process::exit(2);
1187        }
1188        if otlp_interval_ms == 0 {
1189            eprintln!("superfluid: --otlp-interval-ms must be > 0");
1190            std::process::exit(2);
1191        }
1192    }
1193    if fleet_mode && otlp_endpoint.is_some() {
1194        eprintln!(
1195            "superfluid: --otlp-endpoint is not supported in fleet-head mode (the fleet head \
1196             has no scheduler to trace); set it on each node's superfluid instead"
1197        );
1198        std::process::exit(2);
1199    }
1200    let otlp = otlp_endpoint.map(|endpoint| {
1201        let mut cfg = otlp_cfg;
1202        cfg.endpoint = endpoint;
1203        if let Err(e) = cfg.validate() {
1204            eprintln!("superfluid: --otlp-endpoint: {e}");
1205            std::process::exit(2);
1206        }
1207        eprintln!(
1208            "superfluid: pushing operational traces to {} ({}an EGRESS: ids, lengths, timings and \
1209             codes only, never session content; spans: {})",
1210            cfg.traces_url(),
1211            if cfg.is_loopback() { "" } else { "NON-LOOPBACK, " },
1212            cfg.filter
1213        );
1214        cfg
1215    });
1216    use superfluid_daemon::telemetry::{LogConfig, LogSink};
1217    let sink = match log_dir_eff {
1218        Some(dir) => LogSink::File(LogConfig {
1219            dir,
1220            filter: log_filter,
1221            ..Default::default()
1222        }),
1223        None => LogSink::Stderr(log_filter),
1224    };
1225    if let Err(e) = superfluid_daemon::telemetry::init_sinks(sink, tui_ring.clone(), otlp) {
1226        eprintln!("superfluid: telemetry: {e}");
1227    }
1228    std::fs::create_dir_all(&sessions).expect("create sessions dir");
1229    // A fleet head keeps its sessions too: its handlers commit them as a local server's do.
1230    let _sessions_lock = lock_sessions_dir(&sessions);
1231    if sessions_cache {
1232        rotate_session_wal(&sessions);
1233        if let Ok(rd) = std::fs::read_dir(sessions.join("models")) {
1234            for dir in rd.filter_map(|e| e.ok()).map(|e| e.path()).filter(|d| d.is_dir()) {
1235                rotate_session_wal(&dir);
1236            }
1237        }
1238    }
1239    if !fleet_mode {
1240        settle_park_dir(&sessions.join("park"), park, park_budget_gb);
1241        if let Ok(rd) = std::fs::read_dir(sessions.join("models")) {
1242            let mut dirs: Vec<std::path::PathBuf> = rd
1243                .filter_map(|e| e.ok())
1244                .map(|e| e.path().join("park"))
1245                .filter(|d| d.is_dir())
1246                .collect();
1247            dirs.sort();
1248            for dir in dirs {
1249                settle_park_dir(&dir, park, park_budget_gb);
1250            }
1251        }
1252    }
1253
1254    if let Some(b) = kv_bits_arg {
1255        if ![0, 4, 8, 16, 84].contains(&b) {
1256            eprintln!("--kv-bits {b}: expected 0 (auto), 4 (q4_0), 8 (q8_0), 16 (f16) or 84 (K q8_0 / V q4_0)");
1257            std::process::exit(2);
1258        }
1259    }
1260    let kv_bits = match (park_lossy, kv_bits_arg) {
1261        (true, None) | (true, Some(16)) => 16,
1262        (true, Some(other)) => {
1263            eprintln!("--park-lossy requires F16 KV pools; --kv-bits {other} conflicts");
1264            std::process::exit(2);
1265        }
1266        (false, v) => v.unwrap_or(0),
1267    };
1268
1269    let dir_default = max_context.is_none() && !max_context_auto_flag && model_dir.is_some();
1270    // A fleet head holds no model; its window is the nodes', learned when it reaches them.
1271    let max_context_auto = max_context.is_none() && !dir_default && !fleet_mode;
1272    let sizing_lanes = if fim_model.is_some() { max_batch.max(fim_max_batch) } else { max_batch };
1273    // (the window each lane's share of the device holds, the model's
1274    // trained window)
1275    let mut worker_sized: Option<(u64, u64)> = None;
1276    let unsized_why: Option<(String, String)> = if max_context_auto {
1277        let sizing = runtime.id.base();
1278        let elsewhere = models.iter().chain(fim_model.iter()).find_map(|m| {
1279            let on = if fim_model.as_ref() == Some(m) { catalog.reader_of(m) } else { runtime_pick.resolve(m, &catalog) };
1280            on.ok().filter(|id| id.base() != sizing).map(|id| (m, id))
1281        });
1282        match elsewhere {
1283            Some((m, id)) => Some((
1284                format!(
1285                    "--max-context auto sizes the window for the {} runtime, and {} is served by {}",
1286                    sizing.name(),
1287                    m.display(),
1288                    id.base().name()
1289                ),
1290                format!(
1291                    "not sized for this device: the {} runtime sizes it, and {} is served by {}",
1292                    sizing.name(),
1293                    m.display(),
1294                    id.base().name()
1295                ),
1296            )),
1297            None if !superfluid_daemon::linked::sizes_context(sizing) => {
1298                let set: Vec<&PathBuf> = models.iter().chain(fim_model.iter()).collect();
1299                let sized = set
1300                    .iter()
1301                    .map(|m| superfluid_daemon::runtimes::sizing_of(&runtime, m))
1302                    .collect::<Result<Vec<_>, String>>()
1303                    .and_then(|each| {
1304                        let trained = each.iter().map(|s| s.trained_context).min().unwrap_or(0);
1305                        superfluid_adapter_kit::sizing::window(&each, sizing_lanes).map(|n| (n, trained)).ok_or_else(|| {
1306                            format!("the {} runtime reported no device budget to size a window against", sizing.name())
1307                        })
1308                    });
1309                match sized {
1310                    Ok(n) => {
1311                        worker_sized = Some(n);
1312                        None
1313                    }
1314                    Err(why) => Some((format!("--max-context auto: {why}"), why)),
1315                }
1316            }
1317            None => None,
1318        }
1319    } else {
1320        None
1321    };
1322    let max_context_auto = max_context_auto && unsized_why.is_none();
1323    let ctx = if let Some(n) = max_context {
1324        n
1325    } else if fleet_mode {
1326        8192
1327    } else if dir_default {
1328        eprintln!(
1329            "superfluid: context window 8192 tokens (--model-dir loads models on demand at this window, so it is not \
1330             sized to fill the device with the startup model; --max-context N|auto overrides)"
1331        );
1332        8192
1333    } else if let Some((asked, kept)) = unsized_why {
1334        if max_context_auto_flag {
1335            eprintln!("superfluid: {asked}: pass --max-context <tokens>");
1336            std::process::exit(2);
1337        }
1338        eprintln!("superfluid: context window 8192 tokens ({kept}; --max-context N pins it)");
1339        8192
1340    } else if let Some((per_lane, trained)) = worker_sized {
1341        // One conversation may grow past its share of the pool, up to the trained window.
1342        let whole = per_lane.saturating_mul(u64::from(sizing_lanes.max(1)));
1343        let n = i32::try_from(whole.min(trained.max(per_lane))).unwrap_or(i32::MAX);
1344        eprintln!(
1345            "superfluid: context window {n} tokens (auto: sized for this device, {per_lane} for each of {sizing_lanes} lanes{}{}, \
1346             one conversation up to the trained {trained}; --max-context N pins it)",
1347            if models.len() > 1 { format!(", {} models", models.len()) } else { String::new() },
1348            if fim_model.is_some() { ", FIM satellite included" } else { "" },
1349        );
1350        n
1351    } else {
1352        use superfluid_daemon::speculate_resolve::{resolve_all, sizing_path, SpecLoad};
1353        let ranked: Vec<Vec<String>> = models
1354            .iter()
1355            .map(|m| {
1356                if *m == model {
1357                    speculate_primary.iter().chain(speculate_fallbacks.iter()).cloned().collect()
1358                } else {
1359                    // Another model given at startup: its marked drafter is pulled before the sizing
1360                    // counts it, by the runtime that serves it, where that runtime drafts under `auto`.
1361                    let serves = runtime_pick
1362                        .resolve(m, &catalog)
1363                        .and_then(|rt| catalog.require(rt))
1364                        .ok()
1365                        .filter(|rt| rt.static_capabilities().declines_auto_speculation().is_none());
1366                    let pull = serves.map(|rt| auto_drafter_pull(&catalog, rt.id, m));
1367                    let pull = pull.as_ref().filter(|_| !pull_flags.offline).map(|f| f as &dyn Fn(&str));
1368                    speculate.as_deref().and_then(|d| resolve_all(d, m, pull).ok()).unwrap_or_default()
1369                }
1370            })
1371            .collect();
1372        let sizing_models: Vec<PathBuf> = models.iter().cloned().chain(fim_model.iter().cloned()).collect();
1373        let lanes = if fim_model.is_some() { max_batch.max(fim_max_batch) } else { max_batch } as i32;
1374        let speculator_entry = |i: usize, r: &str| -> Option<(PathBuf, bool, Option<usize>)> {
1375            let (path, load) = sizing_path(r, &models[i])?;
1376            Some((path, load == SpecLoad::EmbeddedHead, (load == SpecLoad::Sidecar).then_some(i)))
1377        };
1378        let speculators_for = |choice: &[Option<&String>]| -> Vec<(PathBuf, bool, Option<usize>)> {
1379            (0..models.len()).filter_map(|i| speculator_entry(i, choice[i].or(ranked[i].first())?)).collect()
1380        };
1381        let sizing = runtime.id.base();
1382        let size = |speculators: &[(PathBuf, bool, Option<usize>)]| {
1383            superfluid_daemon::linked::suggest_max_context(sizing, &sizing_models, speculators, lanes, kv_bits).unwrap_or_else(|e| {
1384                eprintln!("superfluid: {e}");
1385                std::process::exit(2);
1386            })
1387        };
1388        let first_picks: Vec<Option<&String>> = vec![None; models.len()];
1389        let n_spec = speculators_for(&first_picks).len();
1390        const MAX_COMBINATIONS: usize = 16;
1391        let counts: Vec<usize> = ranked.iter().map(|c| c.len().max(1)).collect();
1392        let few_enough = counts
1393            .iter()
1394            .try_fold(1usize, |p, &c| p.checked_mul(c).filter(|p| *p <= MAX_COMBINATIONS))
1395            .is_some();
1396        let (sized, note) = if few_enough {
1397            let mut best: Option<(i32, Vec<Option<&String>>)> = None;
1398            let mut idx = vec![0usize; models.len()];
1399            loop {
1400                let choice: Vec<Option<&String>> =
1401                    idx.iter().enumerate().map(|(i, &k)| (k > 0).then(|| &ranked[i][k])).collect();
1402                if let Some(n) = size(&speculators_for(&choice)) {
1403                    if best.as_ref().is_none_or(|(b, _)| n < *b) {
1404                        best = Some((n, choice));
1405                    }
1406                }
1407                let mut i = 0;
1408                while i < idx.len() {
1409                    idx[i] += 1;
1410                    if idx[i] < counts[i] {
1411                        break;
1412                    }
1413                    idx[i] = 0;
1414                    i += 1;
1415                }
1416                if i == idx.len() {
1417                    break;
1418                }
1419            }
1420            let fallbacks: Vec<&str> =
1421                best.iter().flat_map(|(_, c)| c.iter().flatten().map(|s| s.as_str())).collect();
1422            let note = if fallbacks.is_empty() {
1423                String::new()
1424            } else {
1425                format!(", sized for the fallback {}", fallbacks.join(" + "))
1426            };
1427            (best.map(|(n, _)| n), note)
1428        } else {
1429            let all: Vec<(PathBuf, bool, Option<usize>)> = (0..models.len())
1430                .flat_map(|i| ranked[i].iter().filter_map(move |r| speculator_entry(i, r)))
1431                .collect();
1432            let note = format!(", every one of {} drafter candidates charged at once", all.len());
1433            (size(&all), note)
1434        };
1435        match sized {
1436            Some(n) => {
1437                eprintln!(
1438                    "superfluid: context window {n} tokens (auto: sized for this device, {lanes} lanes{}{}{}; --max-context N pins it)",
1439                    if n_spec == 0 { String::new() } else { format!(", {n_spec} speculator(s)") },
1440                    if fim_model.is_some() { ", FIM satellite included" } else { "" },
1441                    note
1442                );
1443                n
1444            }
1445            None => {
1446                eprintln!(
1447                    "superfluid: context window 8192 tokens (auto could not size this device/model set; --max-context N pins it)"
1448                );
1449                8192
1450            }
1451        }
1452    }
1453    .max(512);
1454    if let Some(n) = max_context {
1455        eprintln!("superfluid: context window {ctx} tokens (--max-context {n})");
1456    }
1457    // A worker process starts loading now, while the tokenizer and chat
1458    // template are built: the two do not depend on each other, and the load
1459    // is most of startup.
1460    let spawns_worker = !runtime.in_process_capable() || (worker_process && runtime.worker.bin.is_file());
1461    let early_host = (!fleet_mode && spawns_worker).then(|| {
1462        let spec = superfluid_daemon::WorkerSpec {
1463            bin: runtime.worker.bin.clone(),
1464            args: runtime.serve_args(&model, ctx, max_batch, kv_bits),
1465            stderr: worker_stderr.clone(),
1466        };
1467        std::thread::spawn(move || EngineHost::spawn_process(spec))
1468    });
1469    let tokenizer = open_tokenizer(&runtime, &model).unwrap_or_else(|e| {
1470        eprintln!("superfluid: {e}");
1471        std::process::exit(2);
1472    });
1473    let codec: Box<dyn superfluid_daemon::TextCodec + Send + Sync> = match codec_for_dialect(&dialect, tokenizer) {
1474        Ok((c, kind)) => {
1475            if dialect == "auto" {
1476                match kind {
1477                    "template" => eprintln!("superfluid: serving via the model's own chat template"),
1478                    "raw" => eprintln!(
1479                        "superfluid: model has no chat template; serving raw (chat routes will refuse)"
1480                    ),
1481                    _ => {}
1482                }
1483            }
1484            c
1485        }
1486        Err(e) => {
1487            eprintln!("superfluid: {e}");
1488            std::process::exit(2);
1489        }
1490    };
1491
1492    let store = open_session_store(&sessions);
1493
1494    let workerd_bin = catalog.workerd().to_path_buf();
1495    let worker_process = if !runtime.in_process_capable() {
1496        if !worker_process {
1497            eprintln!(
1498                "superfluid: --no-worker-process: the {} runtime runs in its own worker ({})",
1499                runtime.id.name(),
1500                runtime.worker.bin.display()
1501            );
1502        }
1503        true
1504    } else if worker_process && !runtime.worker.bin.is_file() {
1505        if fim_model.is_some() {
1506            eprintln!(
1507                "superfluid: --fim-model needs superfluid-workerd (the satellite runs in its own \
1508                 worker), not found at {}",
1509                workerd_bin.display()
1510            );
1511            std::process::exit(1);
1512        }
1513        if models.len() > 1 {
1514            eprintln!(
1515                "superfluid: {} models need superfluid-workerd (one address space each), \
1516                 not found at {}",
1517                models.len(),
1518                workerd_bin.display()
1519            );
1520            std::process::exit(1);
1521        }
1522        eprintln!(
1523            "superfluid: superfluid-workerd not found at {}; running the engine in-process \
1524             (install the bundled workerd or pass --no-worker-process to silence)",
1525            workerd_bin.display()
1526        );
1527        false
1528    } else {
1529        worker_process
1530    };
1531    let model_display = model.display().to_string();
1532    let model_source = model.to_string_lossy().into_owned();
1533    let host = if worker_process {
1534        match early_host {
1535            Some(started) => started.join().expect("worker start thread"),
1536            None => EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
1537                bin: runtime.worker.bin.clone(),
1538                args: runtime.serve_args(&model, ctx, max_batch, kv_bits),
1539                stderr: worker_stderr.clone(),
1540            }),
1541        }
1542        .unwrap_or_else(|e| {
1543            #[cfg(feature = "tui")]
1544            if let Some(c) = stderr_capture.as_ref() {
1545                c.drain(std::time::Duration::from_secs(2));
1546            }
1547            eprintln!(
1548                "superfluid: could not start the engine worker for '{model_display}': {e} \
1549                 (the worker's stderr above carries the engine's reason)"
1550            );
1551            std::process::exit(1);
1552        })
1553    } else {
1554        superfluid_daemon::linked::spawn(runtime.id, model, ctx, max_batch, kv_bits)
1555            .unwrap_or_else(|| Err(format!("this build does not link the {} runtime", runtime.id)))
1556            .unwrap_or_else(|e| {
1557                eprintln!("superfluid: could not load '{model_display}': {e}");
1558                std::process::exit(1);
1559            })
1560    };
1561
1562    let caps = host.capabilities().read().expect("capability record").clone();
1563    if let Err(e) = caps.check_startup_flags(park_lossy, kv_bits_arg, speculate.as_deref()) {
1564        eprintln!("superfluid: {e}");
1565        std::process::exit(2);
1566    }
1567    if prefix_blob_budget_mb > 0 {
1568        if let superfluid_daemon::capabilities::Cap::No(why) = caps.get("ops", "space_promote") {
1569            eprintln!("superfluid: --prefix-blob-budget-mb has no effect on runtime {}: {why}", runtime.id.name());
1570        }
1571    }
1572
1573    let listener = socket.as_ref().map(|socket| {
1574        let _ = std::fs::remove_file(socket);
1575        let listener = UnixListener::bind(socket).unwrap_or_else(|e| {
1576            eprintln!("superfluid: cannot listen on --socket {}: {e}", socket.display());
1577            std::process::exit(1);
1578        });
1579        eprintln!("superfluid: serving on {}", socket.display());
1580        listener
1581    });
1582    let park_dir = park.then(|| sessions.join("park"));
1583
1584    let codec: Box<dyn superfluid_daemon::TextCodec + Send + Sync> = match tool_envelope {
1585        Some(env) => {
1586            if let Some(why) = superfluid_daemon::codec::tool_envelope_override_conflict(&*codec, env) {
1587                eprintln!("superfluid: {why}");
1588                std::process::exit(2);
1589            }
1590            if env == codec.tool_envelope() {
1591                // Nothing to override, and wrapping would drop what the envelope does not
1592                // carry: an XML wire reports the JSON envelope but is parsed and streamed as XML.
1593                eprintln!("superfluid: --tool-call-parser {} is this model's own tool wire", env.name());
1594                codec
1595            } else {
1596                eprintln!("superfluid: tool-call envelope overridden to '{}'", env.name());
1597                Box::new(superfluid_daemon::codec::WithToolEnvelope::new(codec, env))
1598            }
1599        }
1600        None => codec,
1601    };
1602    let daemon = Arc::new(
1603        Daemon::with_options(
1604            store,
1605            host,
1606            codec,
1607            superfluid_daemon::DaemonOptions {
1608                max_lanes: max_batch as usize,
1609                park_dir,
1610                park_lossy,
1611                park_budget_bytes: park_budget_gb.saturating_mul(1 << 30),
1612                tool_lease_ms,
1613                tick_decode_budget,
1614                tick_target_ms,
1615                runtime_id: Some(runtime.id.name()),
1616                prefill_budget,
1617                agent_starvation_ticks: starvation_ticks,
1618                class_lanes,
1619                pressure_high_pct: if pressure_high > 0 { pressure_high } else { 85 },
1620                pressure_low_pct: if pressure_low > 0 { pressure_low } else { 70 },
1621                pin_budget_pct,
1622                pressure_source: if os_pressure {
1623                    superfluid_daemon::pressure::PressureConfig::Os
1624                } else {
1625                    superfluid_daemon::pressure::PressureConfig::None
1626                },
1627                speculate: speculate_primary.clone(),
1628                speculate_auto: speculate.as_deref().map(str::trim) == Some("auto"),
1629                speculate_fallbacks: speculate_fallbacks.clone(),
1630                speculation: speculation.clone(),
1631                media_dir: Some(sessions.join("media")),
1632                files_max_bytes,
1633                files_expiry_secs: files_expiry,
1634                completion_deadline_ms,
1635                trace_scope: superfluid_daemon::otlp::TraceScope::model(&model_name),
1636                completion_bucket,
1637            },
1638        )
1639        .unwrap_or_else(|e| {
1640            #[cfg(feature = "tui")]
1641            if let Some(c) = stderr_capture.as_ref() {
1642                c.drain(std::time::Duration::from_secs(2));
1643            }
1644            eprintln!("superfluid: cannot serve '{model_display}': {e}");
1645            std::process::exit(2);
1646        }),
1647    );
1648    {
1649        let bits = daemon.sched_stats().kv_bits.load(std::sync::atomic::Ordering::Relaxed);
1650        match superfluid_daemon::scheduler::kv_bits_label(bits) {
1651            Some(label) if kv_bits != 0 && bits != kv_bits as u64 => {
1652                eprintln!("superfluid: kv cache {label} (--kv-bits {kv_bits} overridden by the engine, see above)")
1653            }
1654            Some(label) => eprintln!("superfluid: kv cache {label}"),
1655            None => {}
1656        }
1657    }
1658    let fim_satellite: Option<(String, Arc<Daemon>)> = fim_model.as_ref().map(|path| {
1659        let runtime = catalog.reader_of(path).and_then(|id| catalog.require(id)).unwrap_or_else(|e| {
1660            eprintln!("superfluid: --fim-model {}: {e}", path.display());
1661            std::process::exit(2);
1662        });
1663        let mut fim_ctx = fim_max_context.unwrap_or(ctx.min(8192)).max(512);
1664        if max_context_auto && fim_ctx > ctx {
1665            eprintln!(
1666                "superfluid: --fim-max-context {fim_ctx} is above the window sized for this device; \
1667                 the FIM satellite runs at {ctx}"
1668            );
1669            fim_ctx = ctx;
1670        }
1671        let sat = build_fim_satellite(&FimSatelliteCfg {
1672            model: path.clone(),
1673            runtime,
1674            sessions: sessions.clone(),
1675            ctx: fim_ctx,
1676            max_batch: fim_max_batch,
1677            kv_bits,
1678            pressure_high,
1679            pressure_low,
1680            os_pressure,
1681            completion_deadline_ms,
1682            worker_stderr: worker_stderr.clone(),
1683        })
1684        .unwrap_or_else(|e| {
1685            #[cfg(feature = "tui")]
1686            if let Some(c) = stderr_capture.as_ref() {
1687                c.drain(std::time::Duration::from_secs(2));
1688            }
1689            eprintln!("superfluid: could not start the --fim-model satellite '{}': {e}", path.display());
1690            std::process::exit(1);
1691        });
1692        let name = superfluid_daemon::registry::model_id_for(path);
1693        eprintln!("superfluid: FIM completions served by satellite '{name}'");
1694        (name, sat)
1695    });
1696    if let Some((name, sat)) = &fim_satellite {
1697        if let Err(e) = daemon.attach_fim_satellite(name.clone(), Arc::clone(sat)) {
1698            eprintln!(
1699                "superfluid: --fim-model {}: {e} (its tokenizer has no atomic <|fim_prefix|>/<|fim_suffix|>/<|fim_middle|> tokens)",
1700                fim_model.as_ref().map(|p| p.display().to_string()).unwrap_or_default()
1701            );
1702            std::process::exit(2);
1703        }
1704    }
1705    let state_names: StateNames = Arc::default();
1706    let registry = if worker_process {
1707        let cfg = ModelBuildCfg {
1708            model_name: embedding.model_name,
1709            base_sessions: sessions.clone(),
1710            state_names: Arc::clone(&state_names),
1711            catalog: Arc::clone(&catalog),
1712            pick: runtime_pick.clone(),
1713            dialect: dialect.clone(),
1714            ctx,
1715            max_batch,
1716            kv_bits,
1717            park,
1718            park_lossy,
1719            park_budget_gb,
1720            tool_lease_ms,
1721            tick_decode_budget,
1722            tick_target_ms,
1723            prefill_budget,
1724            starvation_ticks,
1725            class_lanes,
1726            os_pressure,
1727            pressure_high_pct: if pressure_high > 0 { pressure_high } else { 85 },
1728            pressure_low_pct: if pressure_low > 0 { pressure_low } else { 70 },
1729            pin_budget_pct,
1730            speculate: speculate.clone(),
1731            offline: pull_flags.offline,
1732            speculation: speculation.clone(),
1733            completion_deadline_ms,
1734            completion_bucket,
1735            fim_satellite: fim_satellite.clone(),
1736            worker_stderr: worker_stderr.clone(),
1737        };
1738        let reload_cfg = cfg.clone();
1739        let loader: superfluid_daemon::registry::ModelLoader =
1740            Box::new(move |id: &str, runtime| build_loaded_daemon(&cfg, std::path::Path::new(id), runtime));
1741        let registry = superfluid_daemon::registry::ModelRegistry::with_initial(model_name.clone(), Arc::clone(&daemon), loader);
1742        registry.remember_source_on(&model_name, &model_source, Some(runtime.id));
1743        let names = Arc::clone(&catalog);
1744        registry.set_runtime_names(Box::new(move |name: &str| {
1745            names.named(name).map_err(|why| format!("{why} ({})", names.here_described()))
1746        }));
1747        registry.set_reloader(Box::new(move |id: &str, runtime, retired: &Daemon| {
1748            reload_daemon(&reload_cfg, std::path::Path::new(id), runtime, retired)
1749        }));
1750        Arc::new(registry)
1751    } else {
1752        Arc::new(superfluid_daemon::registry::ModelRegistry::single(
1753            model_name.clone(),
1754            Arc::clone(&daemon),
1755        ))
1756    };
1757    {
1758        let (registry, name) = (Arc::downgrade(&registry), model_name.clone());
1759        daemon.on_retired(Box::new(move || {
1760            if let Some(registry) = registry.upgrade() {
1761                let _ = registry.resolve(Some(&name));
1762            }
1763        }));
1764    }
1765    let same = |p: &std::path::Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
1766    let mut taken: std::collections::HashSet<String> = std::collections::HashSet::from([model_name.clone()]);
1767    let mut given: std::collections::HashSet<PathBuf> = models.first().map(|m| same(m)).into_iter().collect();
1768    for extra in models.iter().skip(1) {
1769        if !given.insert(same(extra)) {
1770            eprintln!("superfluid: model '{}' already loaded, skipping", (embedding.model_name)(extra, &Default::default()));
1771            continue;
1772        }
1773        let name = match pulled_names.get(extra) {
1774            Some(name) => name.clone(),
1775            None => {
1776                let name = (embedding.model_name)(extra, &taken);
1777                // The worker names the model again with nothing taken, so a
1778                // name the collision chose is recorded for it to find.
1779                if name != (embedding.model_name)(extra, &Default::default()) {
1780                    eprintln!("superfluid: {} is registered as '{name}' (another model took its stem)", extra.display());
1781                    state_names.lock().expect("state names").insert(extra.clone(), name.clone());
1782                }
1783                name
1784            }
1785        };
1786        taken.insert(name.clone());
1787        match registry.load_as(&name, &extra.to_string_lossy()) {
1788            Ok(true) => eprintln!("superfluid: loaded model '{name}'"),
1789            Ok(false) => eprintln!("superfluid: model '{name}' already loaded, skipping"),
1790            Err(e) => {
1791                #[cfg(feature = "tui")]
1792                if let Some(c) = stderr_capture.as_ref() {
1793                    c.drain(std::time::Duration::from_secs(2));
1794                }
1795                eprintln!("superfluid: could not load '{}': {e}", extra.display());
1796                std::process::exit(1);
1797            }
1798        }
1799    }
1800    if let Some(dir) = &model_dir {
1801        let mut n = 0;
1802        let here = catalog.survey();
1803        match artifacts_under(dir, &here, MODEL_DIR_DEPTH) {
1804            Ok(paths) => {
1805                for path in paths {
1806                    let refused = runtime_pick
1807                        .resolve(&path, &catalog)
1808                        .and_then(|rt| catalog.require(rt))
1809                        .and_then(|rt| refuse_statically(&rt, park_lossy, kv_bits_arg, speculate.as_deref()));
1810                    if let Err(e) = refused {
1811                        eprintln!("superfluid: --model-dir: skipping {}: {e}", path.display());
1812                        continue;
1813                    }
1814                    if !given.insert(same(&path)) {
1815                        continue;
1816                    }
1817                    let name = (embedding.model_name)(&path, &taken);
1818                    if registry.is_loaded(&name) {
1819                        taken.insert(name);
1820                        continue;
1821                    }
1822                    if name != (embedding.model_name)(&path, &Default::default()) {
1823                        eprintln!(
1824                            "superfluid: --model-dir: {} is registered as '{name}' (another artifact took its stem)",
1825                            path.display()
1826                        );
1827                        state_names.lock().expect("state names").insert(path.clone(), name.clone());
1828                    }
1829                    registry.register_known(&name, &path);
1830                    taken.insert(name);
1831                    n += 1;
1832                }
1833            }
1834            Err(e) => {
1835                eprintln!("superfluid: --model-dir {}: {e}", dir.display());
1836                std::process::exit(1);
1837            }
1838        }
1839        eprintln!("superfluid: {n} model(s) known from {} (loaded on first use)", dir.display());
1840    }
1841    if idle_timeout > 0 {
1842        registry.track_idle();
1843        let reg = Arc::clone(&registry);
1844        let max_idle = std::time::Duration::from_secs(idle_timeout);
1845        let tick = std::time::Duration::from_secs((idle_timeout / 4).max(1));
1846        std::thread::spawn(move || loop {
1847            std::thread::sleep(tick);
1848            for name in reg.sweep_idle(max_idle) {
1849                eprintln!("superfluid: unloaded idle model '{name}'");
1850            }
1851        });
1852    }
1853    if files_expiry.is_some() && files_sweep > 0 {
1854        let d = Arc::clone(&daemon);
1855        std::thread::spawn(move || loop {
1856            std::thread::sleep(std::time::Duration::from_secs(files_sweep));
1857            let n = d.files().sweep();
1858            if n > 0 {
1859                eprintln!("superfluid: files sweep removed {n} expired file(s)");
1860            }
1861        });
1862    }
1863    let mut http_thread = None;
1864    if let Some(addr) = http {
1865        let http_listener = listen_or_exit("--http", &addr);
1866        eprintln!("superfluid: openai api on http://{addr}{}", embedding.http.also_served());
1867        superfluid_daemon::selfupdate::check_in_background(catalog.home().to_path_buf(), pull_flags.offline);
1868        if nonstream_keepalive > 0 {
1869            eprintln!(
1870                "superfluid: non-streaming keep-alive on: 200 at admission, then a space every \
1871                 {nonstream_keepalive}s until the JSON body (x-superfluid-keepalive: whitespace)"
1872            );
1873        }
1874        let reg = Arc::clone(&registry);
1875        let serve_cfg = superfluid_daemon::openai::ServeConfig {
1876            api_key: api_key.clone(),
1877            rate_limit_per_minute: rate_limit,
1878            drain_timeout_secs: drain_timeout,
1879            default_max_tokens,
1880            max_context: ctx as u32,
1881            sampling: superfluid_daemon::openai::SamplingOverrides {
1882                temperature: op_temperature,
1883                top_p: op_top_p,
1884                top_k: op_top_k,
1885                min_p: op_min_p,
1886                repeat_penalty: op_repeat_penalty,
1887                spec_max_temperature: op_spec_max_temperature,
1888                fallback: embedding.sampling_fallback,
1889            },
1890            qos: superfluid_daemon::openai::HttpQosPolicy {
1891                default: http_default_qos,
1892                honor_headers: http_qos_header,
1893                allow_batch_invariant: http_allow_batch_invariant,
1894            },
1895            keys: key_policy.clone(),
1896            nonstream_keepalive_secs: nonstream_keepalive,
1897            surface: embedding.http,
1898            model_naming: embedding.model_naming,
1899            identity: embedding.identity.clone(),
1900            idempotency_dir: Some(sessions.join("idempotency")),
1901        };
1902        http_thread = Some(std::thread::spawn(move || {
1903            superfluid_daemon::openai::serve_blocking_config(http_listener, reg, serve_cfg)
1904        }));
1905    }
1906    if let Some(addr) = web {
1907        if !addr.starts_with("127.0.0.1:") && !addr.starts_with("[::1]:") && !addr.starts_with("localhost:") {
1908            eprintln!("superfluid: --web must bind loopback (127.0.0.1/::1)");
1909            std::process::exit(2);
1910        }
1911        let token_path = sessions.join("web-token");
1912        let token = superfluid_daemon::apiweb::mint_token(&token_path).expect("mint web token");
1913        let web_listener = listen_or_exit("--web", &addr);
1914        eprintln!("superfluid: api-web on http://{addr} (token: {})", token_path.display());
1915        let d = Arc::clone(&daemon);
1916        std::thread::spawn(move || {
1917            let _ = superfluid_daemon::apiweb::serve_blocking(web_listener, d, token, web_origins);
1918        });
1919    }
1920    let _otlp_pusher = otlp_metrics.map(|url| {
1921        eprintln!(
1922            "superfluid: pushing metrics to {url}/v1/metrics every {otlp_interval_ms}ms (an EGRESS)"
1923        );
1924        superfluid_daemon::export::MetricsPusher::start(
1925            url,
1926            std::time::Duration::from_millis(otlp_interval_ms),
1927            daemon.sched_stats(),
1928        )
1929    });
1930    #[cfg(feature = "tui")]
1931    if let Some(ring) = tui_ring {
1932        if let Some(listener) = listener {
1933            let serve_daemon = Arc::clone(&daemon);
1934            std::thread::spawn(move || {
1935                let _ = api::serve(listener, serve_daemon);
1936            });
1937        }
1938        let node = superfluid_daemon::tui::NodeInfo {
1939            identity: hostname(),
1940            model: model_name.clone(),
1941            engine: if worker_process { "native (worker)".into() } else { "native".into() },
1942            max_lanes: max_batch,
1943        };
1944        superfluid_daemon::tui::run(daemon, ring, node, stderr_capture.as_ref()).expect("tui");
1945        drop(stderr_capture);
1946        return;
1947    }
1948    let Some(listener) = listener else {
1949        // No session socket: the HTTP API is the server, and the process
1950        // lasts as long as it does.
1951        match http_thread.map(std::thread::JoinHandle::join) {
1952            Some(Ok(Ok(()))) => return,
1953            Some(Ok(Err(e))) => eprintln!("superfluid: the HTTP API stopped: {e}"),
1954            Some(Err(_)) => eprintln!("superfluid: the HTTP API stopped: its thread panicked"),
1955            None => unreachable!("without a session socket --no-http is refused"),
1956        }
1957        std::process::exit(1);
1958    };
1959    api::serve(listener, daemon).expect("serve");
1960}
1961
1962#[cfg(feature = "tui")]
1963fn hostname() -> String {
1964    let mut buf = [0u8; 256];
1965    // SAFETY: `buf` is a valid 256-byte writable buffer and `buf.len()` is its
1966    // exact capacity; gethostname writes at most that many bytes and NUL-
1967    // terminates within it. We only read `buf` after checking rc == 0.
1968    let rc = unsafe { libc::gethostname(buf.as_mut_ptr().cast::<libc::c_char>(), buf.len()) };
1969    if rc == 0 {
1970        let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1971        String::from_utf8_lossy(&buf[..end]).into_owned()
1972    } else {
1973        "local".into()
1974    }
1975}
1976
1977type StateNames = Arc<std::sync::Mutex<std::collections::HashMap<PathBuf, String>>>;
1978
1979#[derive(Clone)]
1980struct ModelBuildCfg {
1981    model_name: fn(&Path, &std::collections::HashSet<String>) -> String,
1982    base_sessions: std::path::PathBuf,
1983    state_names: StateNames,
1984    catalog: Arc<superfluid_daemon::runtimes::Catalog>,
1985    pick: RuntimePick,
1986    dialect: String,
1987    ctx: i32,
1988    max_batch: u32,
1989    kv_bits: i32,
1990    park: bool,
1991    park_lossy: bool,
1992    park_budget_gb: u64,
1993    tool_lease_ms: u64,
1994    tick_decode_budget: u32,
1995    tick_target_ms: u32,
1996    prefill_budget: u32,
1997    starvation_ticks: u32,
1998    class_lanes: [usize; 4],
1999    os_pressure: bool,
2000    pressure_high_pct: u8,
2001    pressure_low_pct: u8,
2002    pin_budget_pct: u8,
2003    speculate: Option<String>,
2004    /// `--offline`: `auto` pulls no drafter for a model loaded later either.
2005    offline: bool,
2006    speculation: superfluid_daemon::SpeculationOptions,
2007    completion_deadline_ms: u64,
2008    completion_bucket: superfluid_daemon::completion_bucket::BucketConfig,
2009    fim_satellite: Option<(String, Arc<Daemon>)>,
2010    worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>>,
2011}
2012
2013struct FimSatelliteCfg {
2014    model: PathBuf,
2015    runtime: superfluid_daemon::runtimes::Runtime,
2016    sessions: PathBuf,
2017    ctx: i32,
2018    max_batch: u32,
2019    kv_bits: i32,
2020    pressure_high: u8,
2021    pressure_low: u8,
2022    os_pressure: bool,
2023    completion_deadline_ms: u64,
2024    worker_stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>>,
2025}
2026
2027fn build_fim_satellite(cfg: &FimSatelliteCfg) -> Result<Arc<Daemon>, superfluid_daemon::DaemonError> {
2028    use superfluid_daemon::DaemonError;
2029    let tok = open_tokenizer(&cfg.runtime, &cfg.model)
2030        .map_err(|e| DaemonError::Generation(format!("satellite tokenizer load failed: {e}")))?;
2031    let codec = superfluid_daemon::BundleCodec::from_tokenizer(tok);
2032    let host = EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
2033        bin: cfg.runtime.worker.bin.clone(),
2034        args: cfg.runtime.serve_args(&cfg.model, cfg.ctx, cfg.max_batch, cfg.kv_bits),
2035        stderr: cfg.worker_stderr.clone(),
2036    })?;
2037    let daemon = Daemon::with_options(
2038        SessionStore::ephemeral(),
2039        host,
2040        Box::new(codec),
2041        superfluid_daemon::DaemonOptions {
2042            max_lanes: cfg.max_batch as usize,
2043            park_dir: None,
2044            pressure_high_pct: if cfg.pressure_high > 0 { cfg.pressure_high } else { 85 },
2045            pressure_low_pct: if cfg.pressure_low > 0 { cfg.pressure_low } else { 70 },
2046            pressure_source: if cfg.os_pressure {
2047                superfluid_daemon::pressure::PressureConfig::Os
2048            } else {
2049                superfluid_daemon::pressure::PressureConfig::None
2050            },
2051            media_dir: Some(cfg.sessions.join("fim").join("media")),
2052            completion_deadline_ms: cfg.completion_deadline_ms,
2053            trace_scope: superfluid_daemon::otlp::TraceScope::model(
2054                &superfluid_daemon::registry::model_id_for(&cfg.model),
2055            ),
2056            ..Default::default()
2057        },
2058    )?;
2059    Ok(Arc::new(daemon))
2060}
2061
2062fn build_codec_for(
2063    dialect: &str,
2064    runtime: &superfluid_daemon::runtimes::Runtime,
2065    model: &std::path::Path,
2066) -> Result<Box<dyn superfluid_daemon::TextCodec + Send + Sync>, superfluid_daemon::DaemonError> {
2067    use superfluid_daemon::DaemonError;
2068    let tok = open_tokenizer(runtime, model).map_err(DaemonError::Generation)?;
2069    let (codec, _) = codec_for_dialect(dialect, tok).map_err(DaemonError::Generation)?;
2070    Ok(codec)
2071}
2072
2073fn refuse_statically(
2074    runtime: &superfluid_daemon::runtimes::Runtime,
2075    park_lossy: bool,
2076    kv_bits: Option<i32>,
2077    speculate: Option<&str>,
2078) -> Result<(), String> {
2079    runtime.static_capabilities().check_startup_flags(park_lossy, kv_bits, speculate)
2080}
2081
2082fn describe_worker(runtime: &superfluid_daemon::runtimes::Runtime) -> String {
2083    let version = runtime.version.as_deref().map(|v| format!("{v}, ")).unwrap_or_default();
2084    format!("{version}{}: {}", runtime.worker.source.name(), runtime.worker.bin.display())
2085}
2086
2087/// The digest of the tokenizer `model` is read with on `runtime`, as a fleet head and its nodes
2088/// compare them; none when no tokenizer opens here.
2089pub fn tokenizer_digest(runtime: &superfluid_daemon::runtimes::Runtime, model: &std::path::Path) -> Option<u64> {
2090    open_tokenizer(runtime, model).ok().map(|t| superfluid_daemon::fleet::tokenizer_digest(&*t))
2091}
2092
2093pub fn open_tokenizer(
2094    runtime: &superfluid_daemon::runtimes::Runtime,
2095    model: &std::path::Path,
2096) -> Result<Arc<dyn superfluid_engine::Tokenizer>, String> {
2097    use superfluid_engine::artifact::TokenizerSource;
2098    if let Some(why) = not_there(model) {
2099        return Err(why);
2100    }
2101    if let Some(lib) = &runtime.tokenizer {
2102        let tok = superfluid_daemon::dylib_tokenizer::DylibTokenizer::open(lib, model)?;
2103        return Ok(Arc::new(tok));
2104    }
2105    if runtime.info.tokenizer == Some(TokenizerSource::HuggingFace) {
2106        let tok = superfluid_tokenizer_hf::HfTokenizer::load(model)
2107            .map_err(|e| format!("tokenizer load failed for {}: {e}", model.display()))?;
2108        return Ok(Arc::new(tok));
2109    }
2110    superfluid_daemon::linked::tokenizer(runtime.id, model).unwrap_or_else(|| {
2111        Err(format!(
2112            "no tokenizer for {} here: the {} runtime ships none beside its worker, and this superfluid does not link it",
2113            model.display(),
2114            runtime.id
2115        ))
2116    })
2117}
2118
2119/// How deep `--model-dir` looks: a hub cache nests a model three levels
2120/// down (`<org>/<repo>/<variant>/model.base`).
2121const MODEL_DIR_DEPTH: usize = 8;
2122
2123/// The artifacts under `dir` that a runtime here reads, in name order. An
2124/// entry that one reads is taken whole (an MLX model is a directory); a
2125/// directory that none reads is looked into.
2126fn artifacts_under(
2127    dir: &std::path::Path,
2128    here: &[superfluid_daemon::runtimes::Found],
2129    depth: usize,
2130) -> std::io::Result<Vec<PathBuf>> {
2131    let mut paths: Vec<PathBuf> = std::fs::read_dir(dir)?.flatten().map(|e| e.path()).collect();
2132    paths.sort();
2133    let mut found = Vec::new();
2134    for path in paths {
2135        if !superfluid_daemon::runtimes::readers_in(here, &path).is_empty() {
2136            found.push(path);
2137        } else if depth > 0 && path.is_dir() {
2138            found.extend(artifacts_under(&path, here, depth - 1).unwrap_or_default());
2139        }
2140    }
2141    Ok(found)
2142}
2143
2144fn not_there(model: &std::path::Path) -> Option<String> {
2145    (!model.exists()).then(|| format!("{}: no such file or directory", model.display()))
2146}
2147
2148fn codec_for_dialect(
2149    dialect: &str,
2150    tok: Arc<dyn superfluid_engine::Tokenizer>,
2151) -> Result<(Box<dyn superfluid_daemon::TextCodec + Send + Sync>, &'static str), String> {
2152    Ok(match dialect {
2153        "chatml" => (
2154            Box::new(
2155                superfluid_daemon::codec::chatml_codec_from_tokenizer(tok)
2156                    .map_err(|e| format!("--dialect chatml refused: {e}"))?,
2157            ),
2158            "chatml",
2159        ),
2160        "atem" => (
2161            Box::new(
2162                superfluid_daemon::atem::AtemCodec::from_tokenizer(tok)
2163                    .map_err(|_| "--dialect atem refused: the tokenizer lacks the ATEM markers".to_string())?,
2164            ),
2165            "atem",
2166        ),
2167        "raw" => (Box::new(superfluid_daemon::BundleCodec::from_tokenizer(tok)), "raw"),
2168        "template" => (
2169            Box::new(
2170                superfluid_daemon::template_codec::TemplateCodec::from_tokenizer(tok)
2171                    .map_err(|_| "--dialect template refused: the model carries no usable chat template".to_string())?,
2172            ),
2173            "template",
2174        ),
2175        "auto" => superfluid_daemon::codec::auto_codec_from_tokenizer(tok),
2176        other => return Err(format!("unknown dialect '{other}' (chatml|atem|raw|template|auto)")),
2177    })
2178}
2179
2180/// `--speculate auto`'s pull (speculate_resolve::resolve_all) of the drafter the hub catalog marks
2181/// for `target` when no copy is installed, by `runtime`, the one that serves the target, as a drafter
2182/// given by id is pulled. A pull that fails is said, and `auto` settles among what is installed.
2183fn auto_drafter_pull<'a>(
2184    catalog: &'a superfluid_daemon::runtimes::Catalog,
2185    runtime: RuntimeId,
2186    target: &'a std::path::Path,
2187) -> impl Fn(&str) + 'a {
2188    move |id: &str| {
2189        eprintln!("superfluid: speculation: auto: the hub catalog pairs {} with {id}, which is not installed", target.display());
2190        if let Err(e) = superfluid_daemon::pulls::pull(catalog, runtime, id, &superfluid_daemon::pulls::PullFlags::default()) {
2191            eprintln!("superfluid: speculation: auto: {id}: {e} — choosing among what is installed");
2192        }
2193    }
2194}
2195
2196fn speculation_for<'a>(
2197    directive: Option<&'a str>,
2198    caps: &superfluid_daemon::capabilities::Capabilities,
2199    model: &std::path::Path,
2200    runtime: &str,
2201) -> Option<&'a str> {
2202    let d = directive?;
2203    if d.trim() != "auto" {
2204        return Some(d);
2205    }
2206    match caps.declines_auto_speculation() {
2207        Some(why) => {
2208            eprintln!("superfluid: speculation: auto: {} on the {runtime} runtime verifies no drafts ({why}) — decoding plainly", model.display());
2209            None
2210        }
2211        // The other runtimes draft from the prompt alone: there is no
2212        // drafter artifact for `auto` to look for.
2213        None if runtime != "basert" => {
2214            eprintln!("superfluid: speculation: auto: prompt-lookup on the {runtime} runtime");
2215            Some("prompt-lookup")
2216        }
2217        None => Some(d),
2218    }
2219}
2220
2221fn unsupported_runtime(param: Option<&'static str>) -> impl Fn(String) -> superfluid_daemon::DaemonError {
2222    move |m: String| superfluid_daemon::DaemonError::Unsupported(superfluid_daemon::Refusal::new("unsupported_runtime", param, m))
2223}
2224
2225fn runtime_for(
2226    cfg: &ModelBuildCfg,
2227    model_path: &std::path::Path,
2228    requested: Option<RuntimeId>,
2229) -> Result<superfluid_daemon::runtimes::Runtime, superfluid_daemon::DaemonError> {
2230    let runtime = cfg
2231        .pick
2232        .resolve_with(model_path, requested, &cfg.catalog)
2233        .and_then(|rt| cfg.catalog.require(rt))
2234        .map_err(unsupported_runtime(Some(if requested.is_some() { "runtime" } else { "model" })))?;
2235    refuse_statically(&runtime, cfg.park_lossy, Some(cfg.kv_bits), cfg.speculate.as_deref()).map_err(unsupported_runtime(None))?;
2236    Ok(runtime)
2237}
2238
2239fn engine_on(
2240    cfg: &ModelBuildCfg,
2241    runtime: &superfluid_daemon::runtimes::Runtime,
2242    model_path: &std::path::Path,
2243) -> Result<(Box<dyn superfluid_daemon::TextCodec + Send + Sync>, EngineHost), superfluid_daemon::DaemonError> {
2244    use superfluid_daemon::DaemonError;
2245    let codec = build_codec_for(&cfg.dialect, runtime, model_path)?;
2246    let host = EngineHost::spawn_process(superfluid_daemon::WorkerSpec {
2247        bin: runtime.worker.bin.clone(),
2248        args: runtime.serve_args(model_path, cfg.ctx.max(512), cfg.max_batch, cfg.kv_bits),
2249        stderr: cfg.worker_stderr.clone(),
2250    })
2251    .map_err(|_| DaemonError::Generation("engine worker spawn failed".to_string()))?;
2252    host.capabilities()
2253        .read()
2254        .expect("capability record")
2255        .check_startup_flags(cfg.park_lossy, Some(cfg.kv_bits), cfg.speculate.as_deref())
2256        .map_err(unsupported_runtime(None))?;
2257    Ok((codec, host))
2258}
2259
2260fn reload_daemon(
2261    cfg: &ModelBuildCfg,
2262    model_path: &std::path::Path,
2263    requested: Option<RuntimeId>,
2264    retired: &Daemon,
2265) -> Result<(), superfluid_daemon::DaemonError> {
2266    retired.reload_with(|| {
2267        let runtime = runtime_for(cfg, model_path, requested)?;
2268        let (codec, host) = engine_on(cfg, &runtime, model_path)?;
2269        Ok((host, codec))
2270    })
2271}
2272
2273fn build_loaded_daemon(
2274    cfg: &ModelBuildCfg,
2275    model_path: &std::path::Path,
2276    requested: Option<RuntimeId>,
2277) -> Result<Arc<Daemon>, superfluid_daemon::DaemonError> {
2278    use superfluid_daemon::DaemonError;
2279    let id: String = cfg
2280        .state_names
2281        .lock()
2282        .expect("state names")
2283        .get(model_path)
2284        .cloned()
2285        .unwrap_or_else(|| (cfg.model_name)(model_path, &Default::default()));
2286    let mdir = superfluid_daemon::registry::state_dir(&cfg.base_sessions.join("models"), &id)?;
2287    let runtime = runtime_for(cfg, model_path, requested)?;
2288    settle_park_dir(&mdir.join("park"), cfg.park, cfg.park_budget_gb);
2289    let store = SessionStore::open(&mdir.join("wal.log"))?;
2290    let (codec, host) = engine_on(cfg, &runtime, model_path)?;
2291    let park_dir = cfg.park.then(|| mdir.join("park"));
2292    let caps = host.capabilities().read().expect("capability record").clone();
2293    // A model loaded after startup (another --model, --model-dir, /v1/models/load) pulls its marked
2294    // drafter here, as the first model does before it starts.
2295    let pull = auto_drafter_pull(&cfg.catalog, runtime.id, model_path);
2296    let pull = (!cfg.offline).then_some(&pull as &dyn Fn(&str));
2297    let mut speculate_ranked = match speculation_for(cfg.speculate.as_deref(), &caps, model_path, runtime.id.name()) {
2298        Some(d) => superfluid_daemon::speculate_resolve::resolve_all(d, model_path, pull).map_err(DaemonError::Generation)?,
2299        None => Vec::new(),
2300    };
2301    let speculate_first = (!speculate_ranked.is_empty()).then(|| speculate_ranked.remove(0));
2302    let daemon = Daemon::with_options(
2303        store,
2304        host,
2305        codec,
2306        superfluid_daemon::DaemonOptions {
2307            max_lanes: cfg.max_batch as usize,
2308            park_dir,
2309            park_lossy: cfg.park_lossy,
2310            park_budget_bytes: cfg.park_budget_gb.saturating_mul(1 << 30),
2311            tool_lease_ms: cfg.tool_lease_ms,
2312            tick_decode_budget: cfg.tick_decode_budget,
2313            tick_target_ms: cfg.tick_target_ms,
2314            prefill_budget: cfg.prefill_budget,
2315            agent_starvation_ticks: cfg.starvation_ticks,
2316            class_lanes: cfg.class_lanes,
2317            pressure_high_pct: cfg.pressure_high_pct,
2318            pressure_low_pct: cfg.pressure_low_pct,
2319            pin_budget_pct: cfg.pin_budget_pct,
2320            pressure_source: if cfg.os_pressure {
2321                superfluid_daemon::pressure::PressureConfig::Os
2322            } else {
2323                superfluid_daemon::pressure::PressureConfig::None
2324            },
2325            speculate: speculate_first,
2326            speculate_auto: cfg.speculate.as_deref().map(str::trim) == Some("auto"),
2327            speculate_fallbacks: speculate_ranked,
2328            speculation: cfg.speculation.clone(),
2329            media_dir: Some(mdir.join("media")),
2330            completion_deadline_ms: cfg.completion_deadline_ms,
2331            trace_scope: superfluid_daemon::otlp::TraceScope::model(&id),
2332            completion_bucket: cfg.completion_bucket,
2333            runtime_id: Some(runtime.id.name()),
2334            ..Default::default()
2335        },
2336    )?;
2337    if let Some((name, sat)) = &cfg.fim_satellite {
2338        daemon.attach_fim_satellite(name.clone(), Arc::clone(sat))?;
2339    }
2340    Ok(Arc::new(daemon))
2341}
2342
2343fn lock_sessions_dir(sessions: &std::path::Path) -> std::fs::File {
2344    use std::os::fd::AsRawFd;
2345    let path = sessions.join("serve.lock");
2346    let file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&path) {
2347        Ok(file) => file,
2348        Err(e) => {
2349            eprintln!("superfluid: could not open {}: {e}", path.display());
2350            std::process::exit(1);
2351        }
2352    };
2353    loop {
2354        // SAFETY: `file` is an open descriptor for the duration of the call.
2355        let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
2356        if rc == 0 {
2357            return file;
2358        }
2359        let err = std::io::Error::last_os_error();
2360        match err.kind() {
2361            std::io::ErrorKind::Interrupted => continue,
2362            std::io::ErrorKind::WouldBlock => {
2363                eprintln!(
2364                    "superfluid: another server is already using the sessions directory {}. \
2365                     Stop it, or give this one its own --sessions directory.",
2366                    sessions.display()
2367                );
2368                std::process::exit(1);
2369            }
2370            _ => {
2371                eprintln!("superfluid: could not lock {}: {err}", path.display());
2372                std::process::exit(1);
2373            }
2374        }
2375    }
2376}
2377
2378fn rotate_session_wal(dir: &std::path::Path) {
2379    use superfluid_daemon::store_id::store_id_path;
2380    let wal = dir.join("wal.log");
2381    let prev = dir.join("wal.log.prev");
2382    let keep = std::fs::metadata(&wal).is_ok_and(|m| m.len() > 0);
2383    let aside = |from: &std::path::Path, to: Option<&std::path::Path>| -> bool {
2384        if !from.exists() {
2385            return false;
2386        }
2387        if to.is_some_and(|to| std::fs::rename(from, to).is_ok()) {
2388            return true;
2389        }
2390        if let Err(e) = std::fs::remove_file(from) {
2391            eprintln!(
2392                "superfluid: --sessions-cache could not set aside or remove {} ({e}); \
2393                 remove it, or run without --sessions-cache.",
2394                from.display()
2395            );
2396            std::process::exit(1);
2397        }
2398        false
2399    };
2400    if keep {
2401        let _ = std::fs::remove_file(store_id_path(&prev));
2402    }
2403    if keep && aside(&wal, Some(&prev)) {
2404        aside(&store_id_path(&wal), Some(&store_id_path(&prev)));
2405    } else {
2406        aside(&wal, None);
2407        aside(&store_id_path(&wal), None);
2408    }
2409}
2410#[cfg(test)]
2411mod tests {
2412    use super::*;
2413    use superfluid_daemon::runtimes::{Found, Info};
2414    use superfluid_engine::artifact::Format;
2415
2416    fn reads(id: &'static str, format: Format) -> Found {
2417        let info = Info { aliases: Vec::new(), formats: vec![format], tokenizer: None, named: None };
2418        Found { id: RuntimeId::new(id), worker: None, info: Some(info), usable: Err("not started".into()) }
2419    }
2420
2421    #[test]
2422    fn model_dir_looks_into_a_hub_cache_and_takes_a_directory_model_whole() {
2423        let root = std::env::temp_dir().join(format!("superfluid-model-dir-{}", std::process::id()));
2424        let _ = std::fs::remove_dir_all(&root);
2425        let file = |rel: &str| {
2426            let p = root.join(rel);
2427            std::fs::create_dir_all(p.parent().unwrap()).unwrap();
2428            std::fs::write(&p, b"BASE....").unwrap();
2429            p
2430        };
2431        let flat = file("Flat-Q4.base");
2432        let q4 = file("org/Model/default-q4/model.base");
2433        file("org/Model/default-q4/hub.json");
2434        let q8 = file("org/Model/default-q8/model.base");
2435        file("notes.txt");
2436        file("mlx-model/config.json");
2437        file("mlx-model/weights.safetensors");
2438        file("mlx-model/stray.base");
2439        let here = [
2440            reads("basert", Format::file("base", "a .base bundle", &["base"], None)),
2441            reads("mlx", Format::directory("mlx", "an MLX model", &["config.json"], &["safetensors"])),
2442        ];
2443        let found = artifacts_under(&root, &here, MODEL_DIR_DEPTH).unwrap();
2444        assert_eq!(found, vec![flat, root.join("mlx-model"), q4, q8]);
2445        assert!(artifacts_under(&root, &here, 0).unwrap().len() == 2, "depth 0 reads only the top level");
2446    }
2447}