1pub mod anthropic;
4pub mod api;
5pub mod apiweb;
6pub mod atem;
7pub mod bus;
8pub mod capabilities;
9pub mod cli;
10pub mod compat;
11pub mod codec;
12mod exec;
13pub mod completion_bucket;
14pub mod files;
15pub mod harmony;
16pub mod keypolicy;
17pub mod launch;
18pub mod batches;
19pub mod response_store;
20pub mod ratelimit;
21pub mod registry;
22pub mod template_codec;
23pub mod tool_fragment;
24pub mod fleet;
25pub mod fleet_manager;
26pub mod fleet_warm;
27pub(crate) mod fleet_daemon;
28pub mod idempotency;
29pub mod fleet_join;
30pub mod fleet_mdns;
31pub mod fleet_token;
32#[cfg(feature = "tui")]
33pub mod tui;
34#[cfg(feature = "tui")]
35pub mod tui_top;
36pub mod nodeagent;
37mod inference;
38mod block_inference;
39mod generation_output;
40mod openai_output;
41mod http_state;
42mod http_generation;
43mod keepalive;
44mod responses;
45pub mod openai;
46pub mod ollama;
47pub mod fleet_metrics;
48pub mod park;
49pub mod pins;
50pub mod runtime;
51pub mod runtime_options;
52pub mod runtime_pick;
53pub mod installs;
54pub mod pulls;
55pub mod runtimes;
56pub mod runtimes_report;
57pub mod versions;
58pub mod selfupdate;
59pub mod linked;
60pub mod dylib_tokenizer;
61pub mod export;
62pub mod otlp;
63pub mod media;
64pub mod metrics;
65pub mod pressure;
66pub mod scheduler;
67pub mod serve;
68pub mod speculate_resolve;
69use superfluid_proto::linkw as proto_linkw;
70pub mod telemetry;
71pub mod store;
72pub mod store_id;
73pub mod wal;
74pub mod workerd;
75
76pub mod qos {
77 pub const INTERACTIVE_CHAT: u8 = 0;
78 pub const INLINE_COMPLETION: u8 = 1;
79 pub const FOREGROUND_AGENT: u8 = 2;
80 pub const BACKGROUND_AGENT: u8 = 3;
81
82 pub const NAMES: [&str; 4] = ["interactive", "completion", "agent", "background"];
83
84 pub fn parse(s: &str) -> Option<u8> {
85 match s.trim().to_ascii_lowercase().as_str() {
86 "interactive" | "interactive-chat" | "interactive_chat" | "0" => Some(INTERACTIVE_CHAT),
87 "completion" | "inline-completion" | "inline_completion" | "1" => Some(INLINE_COMPLETION),
88 "agent" | "foreground" | "foreground-agent" | "foreground_agent" | "2" => Some(FOREGROUND_AGENT),
89 "background" | "background-agent" | "background_agent" | "3" => Some(BACKGROUND_AGENT),
90 _ => None,
91 }
92 }
93
94 pub fn parse_class_lanes(spec: &str, max_lanes: usize) -> Result<[usize; 4], String> {
95 let mut caps = [0usize; 4];
96 let mut seen = [false; 4];
97 for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
98 let (name, n) = part
99 .split_once('=')
100 .ok_or_else(|| format!("expected class=N, got {part:?}"))?;
101 let class = parse(name).ok_or_else(|| {
102 format!("unknown class {:?} (expected one of {})", name.trim(), NAMES.join(", "))
103 })? as usize;
104 if seen[class] {
105 return Err(format!("class {} is capped twice", NAMES[class]));
106 }
107 seen[class] = true;
108 let n: usize = n
109 .trim()
110 .parse()
111 .map_err(|_| format!("cap for {} must be a whole number, got {:?}", NAMES[class], n.trim()))?;
112 if n == 0 {
113 return Err(format!(
114 "cap for {} must be at least 1 (omit the class to leave it uncapped)",
115 NAMES[class]
116 ));
117 }
118 if n > max_lanes {
119 return Err(format!(
120 "cap {n} for {} exceeds --max-batch {max_lanes}; it could never bind",
121 NAMES[class]
122 ));
123 }
124 caps[class] = n;
125 }
126 Ok(caps)
127 }
128
129 #[cfg(test)]
130 mod tests {
131 use super::*;
132
133 #[test]
134 fn names_round_trip_and_aliases_parse() {
135 for (c, name) in NAMES.iter().enumerate() {
136 assert_eq!(parse(name), Some(c as u8));
137 assert_eq!(parse(&c.to_string()), Some(c as u8));
138 }
139 assert_eq!(parse(" Interactive-Chat "), Some(INTERACTIVE_CHAT));
140 assert_eq!(parse("foreground_agent"), Some(FOREGROUND_AGENT));
141 assert_eq!(parse("urgent"), None);
142 assert_eq!(parse("4"), None);
143 }
144
145 #[test]
146 fn class_lanes_spec_parses_and_refuses_bad_caps() {
147 assert_eq!(parse_class_lanes("", 8), Ok([0; 4]));
148 assert_eq!(parse_class_lanes("background=2, agent=6", 8), Ok([0, 0, 6, 2]));
149 assert_eq!(parse_class_lanes("interactive=8", 8), Ok([8, 0, 0, 0]));
150 assert!(parse_class_lanes("background", 8).unwrap_err().contains("class=N"));
151 assert!(parse_class_lanes("urgent=1", 8).unwrap_err().contains("unknown class"));
152 assert!(parse_class_lanes("agent=-1", 8).unwrap_err().contains("whole number"));
153 assert!(parse_class_lanes("agent=9", 8).unwrap_err().contains("exceeds"));
154 assert!(parse_class_lanes("background=0", 8).unwrap_err().contains("at least 1"));
155 assert!(parse_class_lanes("agent=1,foreground=2", 8).unwrap_err().contains("twice"));
156 }
157 }
158}
159
160pub use codec::{MockCodec, NoCodec, TextCodec, Utf8Stream};
161pub use superfluid_engine::StageOutput;
162pub use runtime::{EngineHost, WorkerSpec};
163pub use store::{CommittedEvent, SessionStore};
164pub use wal::{EventBody, GenParams};
165
166pub use codec::BundleCodec;
167
168#[derive(Debug, thiserror::Error)]
169pub enum DaemonError {
170 #[error("io: {0}")]
171 Io(#[from] std::io::Error),
172 #[error("wal encode/decode: {0}")]
173 Codec(#[from] postcard::Error),
174 #[error("wal record failed checksum")]
175 WalCorrupt,
176 #[error("unknown session {0}")]
177 UnknownSession(u64),
178 #[error("session {0} has no content to generate from")]
179 EmptySession(u64),
180 #[error(
181 "this model's maximum context length is {max} tokens, but your request has {len} \
182 tokens; shorten the prompt or drop earlier turns"
183 )]
184 StreamTooLong { len: u64, max: u64 },
185 #[error("model load: {0}")]
186 EngineLoad(String),
187 #[error("engine link: {0}")]
188 Agent(#[from] superfluid_agent::AgentError),
189 #[error("fleet link (Link F): {0}")]
190 Fleet(#[from] superfluid_linkf::LinkFError),
191 #[error("fleet node {addr}: {why}")]
192 FleetNode { addr: String, why: String },
193 #[error("engine rejected the plan with status {0}")]
194 Rejected(i32),
195 #[error("generation failed: {0}")]
196 Generation(String),
197 #[error("session {0} is generating; append/generate after it finishes or cancel it")]
198 SessionBusy(u64),
199 #[error("tool call {0} is unknown or already closed")]
200 UnknownToolCall(u64),
201 #[error("unknown message role {0}")]
202 UnknownRole(u32),
203 #[error("tool {index} is not a function declaration: {why}")]
204 InvalidTool { index: usize, why: String },
205 #[error("session {0} already has a prompt; AppendSystem must come before any other turn")]
206 SystemNotFirst(u64),
207 #[error("fork point {fork_at} is outside session {session}'s committed events (1..={max})")]
208 ForkPoint { session: u64, fork_at: u64, max: u64 },
209 #[error("tool call {0} belongs to an ancestor's timeline; only that session can close it")]
210 InheritedToolCall(u64),
211 #[error("session {session} metadata version is {actual}, command expected {expected}")]
212 MetaConflict { session: u64, expected: u64, actual: u64 },
213 #[error("session {session} generation is {actual}, command expected {expected}")]
214 GenerationConflict { session: u64, expected: u64, actual: u64 },
215 #[error("session {0} was purged")]
216 Purged(u64),
217 #[error("files quota exceeded: {stored} bytes stored, {incoming} more would pass the {max} limit")]
218 FilesQuota { stored: u64, incoming: u64, max: u64 },
219 #[error("rebase edit spec invalid: {0}")]
220 RebaseSpec(&'static str),
221 #[error("session {0} did not quiesce for purge (lane still running)")]
222 PurgeTimeout(u64),
223 #[error("permission request {0} is unknown or already answered")]
224 UnknownPermission(u64),
225 #[error("session {0} has an open permission request; answer it before generating")]
226 PermissionPending(u64),
227 #[error("tool call {0}'s executor lease expired; the result was recorded as expired, not applied")]
228 ToolLeaseExpired(u64),
229 #[error("tool call {0} already reached a terminal outcome; the late result was recorded as a reconciliation")]
230 ToolLateEffect(u64),
231 #[error("unknown block kind {0}")]
232 UnknownBlockKind(u32),
233 #[error("media blob {0} is not in the pool (put it first)")]
234 UnknownMedia(String),
235 #[error("image expands to {0} placeholders, beyond one prefill chunk")]
236 MediaTooLarge(u32),
237 #[error("this codec has no FIM dialect")]
238 NoFimDialect,
239 #[error("completion rate limit exceeded; retry in {retry_after_ms} ms")]
240 CompletionThrottled { retry_after_ms: u64 },
241 #[error("protocol: {0}")]
242 Protocol(&'static str),
243 #[error("constraint unsatisfiable: {0}")]
244 Constraint(String),
245 #[error("the model's chat template refused this conversation: {0}")]
246 TemplateRefused(String),
247 #[error("{0}")]
248 Unsupported(Refusal),
249 #[error("configuration: {0}")]
250 Config(&'static str),
251 #[error("fleet: {0}")]
253 NoNode(&'static str),
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct Refusal {
258 pub code: &'static str,
259 pub param: Option<String>,
260 pub message: String,
261}
262
263impl Refusal {
264 pub fn new(code: &'static str, param: Option<&str>, message: impl Into<String>) -> Refusal {
265 Refusal { code, param: param.map(str::to_string), message: message.into() }
266 }
267}
268
269impl std::fmt::Display for Refusal {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.write_str(&self.message)
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
276pub struct SessionSummary {
277 pub id: u64,
278 pub parent: Option<u64>,
279 pub fork_at: u64,
280 pub title: Option<String>,
281 pub archived: bool,
282 pub meta_version: u64,
283 pub generation: u64,
284 pub events: u64,
285 pub tokens: u64,
286 pub open_tool_calls: u64,
287}
288
289#[derive(Debug)]
290pub struct GenerateOutcome {
291 pub events: Vec<CommittedEvent>,
292 pub tokens_generated: u32,
293 pub finish: u32,
294 pub warm_prefix: u64,
295 pub logprobs: Vec<scheduler::TokenLogprob>,
296 pub spec: (u64, u64),
297 pub decode: (u64, f64),
300}
301
302#[derive(Debug, Clone, Default, PartialEq)]
303pub struct ModelSamplingDefaults {
304 pub temperature: Option<f32>,
305 pub top_p: Option<f32>,
306 pub top_k: Option<u32>,
307 pub min_p: Option<f32>,
308 pub do_sample: Option<bool>,
309 pub repetition_penalty: Option<f32>,
310}
311
312impl ModelSamplingDefaults {
313 pub fn from_json(v: &serde_json::Value) -> Self {
314 let f = |k: &str| v.get(k).and_then(|x| x.as_f64()).map(|x| x as f32);
315 Self {
316 temperature: f("temperature"),
317 top_p: f("top_p"),
318 top_k: v.get("top_k").and_then(|x| x.as_u64()).map(|x| x as u32),
319 min_p: f("min_p"),
320 do_sample: v.get("do_sample").and_then(|x| x.as_bool()),
321 repetition_penalty: f("repetition_penalty"),
322 }
323 }
324}
325
326thread_local! {
327 static SYNC_CLASS: std::cell::Cell<Option<u8>> = const { std::cell::Cell::new(None) };
328}
329
330pub fn with_sync_class<R>(class: Option<u8>, f: impl FnOnce() -> R) -> R {
331 let _scope = SyncClassScope::enter(class);
332 f()
333}
334
335pub struct SyncClassScope {
336 prev: Option<Option<u8>>,
337 _not_send: std::marker::PhantomData<*const ()>,
338}
339
340impl SyncClassScope {
341 pub fn enter(class: Option<u8>) -> SyncClassScope {
342 let prev = class.map(|c| SYNC_CLASS.with(|cell| cell.replace(Some(c))));
343 SyncClassScope { prev, _not_send: std::marker::PhantomData }
344 }
345}
346
347impl Drop for SyncClassScope {
348 fn drop(&mut self) {
349 if let Some(prev) = self.prev {
350 SYNC_CLASS.with(|c| c.set(prev));
351 }
352 }
353}
354
355fn ambient_sync_class() -> Option<u8> {
356 SYNC_CLASS.with(|c| c.get())
357}
358
359struct Serving {
360 capabilities: runtime::SharedCapabilities,
361 retired: std::sync::Arc<std::sync::Mutex<Option<String>>>,
362 model_sampling: std::sync::OnceLock<ModelSamplingDefaults>,
363 codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
364 sched: scheduler::SchedulerHandle,
365 speculation: std::sync::Arc<scheduler::Speculation>,
366 staged_tokens: u64,
367}
368
369#[derive(Default)]
373pub struct CancelSet {
374 set: std::sync::Mutex<std::collections::HashSet<u64>>,
375 wake: std::sync::RwLock<Option<runtime::YieldSlot>>,
376 latency_at_ms: std::sync::atomic::AtomicU64,
379}
380
381fn monotonic_ms() -> u64 {
384 static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
385 BASE.get_or_init(std::time::Instant::now).elapsed().as_millis() as u64 + 1
386}
387
388impl std::ops::Deref for CancelSet {
389 type Target = std::sync::Mutex<std::collections::HashSet<u64>>;
390
391 fn deref(&self) -> &Self::Target {
392 &self.set
393 }
394}
395
396impl CancelSet {
397 pub fn cancel(&self, session: u64) {
398 self.set.lock().expect("cancel registry").insert(session);
399 self.wake();
400 }
401
402 pub fn wake(&self) {
404 if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
405 runtime::raise_yield(slot);
406 }
407 }
408
409 pub(crate) fn wake_with(&self, slot: runtime::YieldSlot) {
410 *self.wake.write().expect("wake slot") = Some(slot);
411 }
412
413 pub fn latency_request(&self) {
416 self.latency_at_ms.store(monotonic_ms(), std::sync::atomic::Ordering::Release);
417 if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
418 runtime::set_latency(slot, true);
419 }
420 self.wake();
421 }
422
423 pub(crate) fn latency_recent(&self, hold: std::time::Duration) -> bool {
425 let at = self.latency_at_ms.load(std::sync::atomic::Ordering::Acquire);
426 at > 0 && monotonic_ms().saturating_sub(at) < hold.as_millis() as u64
427 }
428}
429
430struct Ground<'a> {
431 store: &'a std::sync::Arc<std::sync::Mutex<SessionStore>>,
432 cancels: &'a std::sync::Arc<CancelSet>,
433 active: &'a std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
434 stats: &'a std::sync::Arc<scheduler::SchedStats>,
435 background_tick_divisor: &'a std::sync::Arc<std::sync::atomic::AtomicU32>,
436}
437
438impl Serving {
439 fn over_fleet(
440 codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
441 ground: Ground<'_>,
442 fleet: std::sync::Arc<fleet_manager::FleetManager>,
443 model: String,
444 opts: DaemonOptions,
445 ) -> Serving {
446 let capabilities = std::sync::Arc::new(std::sync::RwLock::new(fleet_daemon::capability_record()));
447 let sched = fleet_daemon::spawn(fleet_daemon::Dispatch {
448 fleet,
449 model,
450 store: std::sync::Arc::clone(ground.store),
451 codec: std::sync::Arc::clone(&codec),
452 cancels: std::sync::Arc::clone(ground.cancels),
453 active: std::sync::Arc::clone(ground.active),
454 stats: std::sync::Arc::clone(ground.stats),
455 tool_lease_ms: opts.tool_lease_ms,
456 handles: Default::default(),
457 capabilities: std::sync::Arc::clone(&capabilities),
458 });
459 Serving {
460 capabilities,
461 retired: std::sync::Arc::new(std::sync::Mutex::new(None)),
462 model_sampling: std::sync::OnceLock::new(),
463 codec,
464 sched,
465 speculation: std::sync::Arc::new(scheduler::Speculation::default()),
466 staged_tokens: 0,
467 }
468 }
469
470 fn start(
471 host: EngineHost,
472 codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
473 ground: Ground<'_>,
474 opts: DaemonOptions,
475 ) -> Result<Serving, DaemonError> {
476 let mut opts = opts;
477 let mut speculation = std::sync::Arc::new(scheduler::Speculation::default());
478 let mut host = host;
479 ground.cancels.wake_with(host.yield_slot());
480 ground
481 .stats
482 .kv_bits
483 .store(host.kv_bits() as u64, std::sync::atomic::Ordering::Relaxed);
484 if let Some(first) = opts.speculate.clone() {
485 let mut candidates = vec![first];
486 if opts.speculate_auto {
487 candidates.extend(opts.speculate_fallbacks.iter().cloned());
488 }
489 let picked = first_registrable(
490 &candidates,
491 opts.speculate_auto,
492 |strategy| {
493 speculation = std::sync::Arc::new(scheduler::Speculation::default());
494 scheduler::Speculation::register(&speculation, &mut host, strategy, &opts.speculation)
495 },
496 |strategy, e, next| {
497 eprintln!(
498 "superfluid: speculation: auto picked {strategy}, but the engine refused it ({e}) — {}",
499 match next {
500 Some(n) => format!("trying {n}"),
501 None => "decoding plainly".to_string(),
502 }
503 );
504 },
505 )?;
506 if picked.is_none() {
507 speculation = std::sync::Arc::new(scheduler::Speculation::default());
508 } else if picked != opts.speculate {
509 eprintln!("superfluid: speculation: {}", picked.as_deref().unwrap_or_default());
510 }
511 opts.speculate = picked;
512 }
513 let staged_tokens = host.staged_tokens();
514 let capabilities = host.capabilities();
515 let retired = std::sync::Arc::new(std::sync::Mutex::new(None));
516 let sched = scheduler::Scheduler::spawn(
517 host,
518 std::sync::Arc::clone(ground.store),
519 std::sync::Arc::clone(&codec),
520 std::sync::Arc::clone(ground.cancels),
521 std::sync::Arc::clone(ground.active),
522 std::sync::Arc::clone(ground.stats),
523 std::sync::Arc::clone(ground.background_tick_divisor),
524 std::sync::Arc::clone(&speculation),
525 std::sync::Arc::clone(&retired),
526 opts,
527 );
528 {
529 let st = ground.store.lock().expect("store");
530 let now = wal::now_unix_ms();
531 for id in st.session_ids() {
532 let Ok(s) = st.session(id) else { continue };
533 if s.pin_deadline_unix_ms > now {
534 let (tokens, want) = pins::session_target(&s.tokens);
535 let _ = sched.tx.send(scheduler::SchedCmd::Pin(pins::PinReq {
536 key: pins::PinKey::Session(id),
537 tokens,
538 want,
539 deadline_unix_ms: s.pin_deadline_unix_ms,
540 anchor: Some(id),
541 reply: None,
542 }));
543 }
544 }
545 }
546 Ok(Serving { capabilities, retired, model_sampling: std::sync::OnceLock::new(), codec, sched, speculation, staged_tokens })
547 }
548}
549
550pub struct Daemon {
551 serving: std::sync::RwLock<std::sync::Arc<Serving>>,
552 reloading: std::sync::Mutex<()>,
553 reload_hook: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>>,
554 runtime_id: Option<&'static str>,
555 store: std::sync::Arc<std::sync::Mutex<SessionStore>>,
556 cancels: std::sync::Arc<CancelSet>,
557 active: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
558 purging: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
559 stats: std::sync::Arc<scheduler::SchedStats>,
560 trace_scope: otlp::TraceScope,
561 background_tick_divisor: std::sync::Arc<std::sync::atomic::AtomicU32>,
562 park_dir: Option<std::path::PathBuf>,
563 tool_lease_ms: u64,
564 media: media::MediaPool,
565 files: files::FileStore,
566 batches: std::sync::Arc<batches::BatchStore>,
567 responses: response_store::ResponseStore,
568 completion: std::sync::Arc<CompletionSatellite>,
569 fim_satellite: std::sync::OnceLock<FimSatellite>,
570 built_with: DaemonOptions,
571 fleet: Option<(std::sync::Arc<fleet_manager::FleetManager>, String)>,
573}
574
575impl CompletionSatellite {
576 fn cache_get(&self, key: u64) -> Option<(Vec<u32>, String, u32)> {
577 let mut c = self.cache.lock().expect("completion cache");
578 if let Some(v) = c.map.get(&key).cloned() {
579 c.lru.retain(|&k| k != key);
580 c.lru.push_back(key);
581 Some(v)
582 } else {
583 None
584 }
585 }
586 fn cache_put(&self, key: u64, tokens: Vec<u32>, text: String, finish: u32) {
587 let mut c = self.cache.lock().expect("completion cache");
588 if c.map.insert(key, (tokens, text, finish)).is_none() {
589 c.lru.push_back(key);
590 }
591 while c.lru.len() > self.cache_cap {
592 if let Some(old) = c.lru.pop_front() {
593 c.map.remove(&old);
594 }
595 }
596 }
597}
598
599struct FimSatellite {
600 name: String,
601 daemon: std::sync::Arc<Daemon>,
602}
603
604#[derive(Default)]
605struct CompletionCounters {
606 generated: std::sync::atomic::AtomicU64,
607 cached: std::sync::atomic::AtomicU64,
608 expired: std::sync::atomic::AtomicU64,
609 throttled: std::sync::atomic::AtomicU64,
610}
611
612struct CompletionSatellite {
613 store: std::sync::Mutex<SessionStore>,
614 cache: std::sync::Mutex<CompletionCache>,
615 deadline_ms: u64,
616 cache_cap: usize,
617 bucket: completion_bucket::BucketConfig,
618 counters: CompletionCounters,
619}
620
621#[derive(Default)]
622struct CompletionCache {
623 map: std::collections::HashMap<u64, (Vec<u32>, String, u32)>,
624 lru: std::collections::VecDeque<u64>,
625}
626
627#[derive(Debug, Clone, PartialEq)]
628pub struct Completion {
629 pub text: String,
630 pub tokens: Vec<u32>,
631 pub cached: bool,
632 pub expired: bool,
633 pub finish: u32,
634 pub prompt_tokens: u32,
635}
636
637#[derive(Debug, Clone)]
638pub struct FimRequest {
639 pub prefix: String,
640 pub suffix: String,
641 pub mode: u8,
642 pub max_tokens: u32,
643 pub params: GenParams,
644 pub extras: scheduler::GenExtras,
645}
646
647impl FimRequest {
648 pub fn greedy(prefix: &str, suffix: &str, mode: u8, max_tokens: u32) -> FimRequest {
649 FimRequest {
650 prefix: prefix.to_string(),
651 suffix: suffix.to_string(),
652 mode,
653 max_tokens,
654 params: GenParams::default(),
655 extras: scheduler::GenExtras::default(),
656 }
657 }
658}
659
660#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
661pub enum Tier {
662 Resident,
663 Parked { covered: u64, encoding: u8 },
664 Cold,
665}
666
667#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
668pub struct Inspection {
669 pub summary: SessionSummary,
670 pub params: GenParams,
671 pub epoch: u64,
672 pub last_finish: u32,
673 pub qos_class: u8,
674 pub batch_invariant: bool,
675 pub pin_deadline_unix_ms: u64,
676 pub behavior_fingerprint: Option<u64>,
677 pub open_tool_calls: Vec<(u64, String, String)>,
678 pub tier: Tier,
679 pub tokens: Vec<u32>,
680 pub events: Vec<api::EventMsg>,
681 pub wal_version: u8,
682 #[serde(default, deserialize_with = "de_trailing_opt")]
683 pub store_id: Option<store::StoreId>,
684}
685
686fn de_trailing_opt<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
687where
688 D: serde::Deserializer<'de>,
689 T: serde::Deserialize<'de>,
690{
691 use std::cell::Cell;
692 struct V<'a, T>(&'a Cell<bool>, std::marker::PhantomData<T>);
693 impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for V<'_, T> {
694 type Value = Option<T>;
695 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
696 f.write_str("an optional trailing field")
697 }
698 fn visit_none<E: serde::de::Error>(self) -> Result<Option<T>, E> {
699 self.0.set(true);
700 Ok(None)
701 }
702 fn visit_some<D2: serde::Deserializer<'de>>(self, d: D2) -> Result<Option<T>, D2::Error> {
703 self.0.set(true);
704 T::deserialize(d).map(Some)
705 }
706 }
707 let reached = Cell::new(false);
708 match d.deserialize_option(V(&reached, std::marker::PhantomData)) {
709 Ok(v) => Ok(v),
710 Err(e) if !reached.get() && e.to_string() == POSTCARD_UNEXPECTED_END => Ok(None),
711 Err(e) => Err(e),
712 }
713}
714
715const POSTCARD_UNEXPECTED_END: &str = "Hit the end of buffer, expected more data";
716
717#[derive(Debug, Clone, Default, PartialEq)]
718pub struct SpeculationOptions {
719 pub max_draft: Option<u32>,
720 pub adaptive: Option<bool>,
721 pub min_yield: Option<f64>,
722 pub yield_rounds: Option<u32>,
723 pub throughput_gate: Option<bool>,
724 pub gate_probe_tokens: Option<u32>,
725 pub min_speedup: Option<f64>,
726 pub gate_reprobe: Option<u32>,
727 pub gate_reprobe_max: Option<u32>,
728 pub dspark_confidence: Option<f32>,
729}
730
731#[derive(Debug, Clone)]
732pub struct DaemonOptions {
733 pub max_lanes: usize,
734 pub park_dir: Option<std::path::PathBuf>,
735 pub pressure_high_pct: u8,
736 pub pressure_low_pct: u8,
737 pub pin_budget_pct: u8,
738 pub park_lossy: bool,
739 pub park_budget_bytes: u64,
740 pub class_lanes: [usize; 4],
741 pub tick_decode_budget: u32,
742 pub tick_target_ms: u32,
743 pub runtime_id: Option<&'static str>,
744 pub prefill_budget: u32,
745 pub agent_starvation_ticks: u32,
746 pub pressure_source: pressure::PressureConfig,
747 pub completion_deadline_ms: u64,
748 pub completion_bucket: completion_bucket::BucketConfig,
749 pub media_dir: Option<std::path::PathBuf>,
750 pub files_max_bytes: Option<u64>,
751 pub files_expiry_secs: Option<u64>,
752 pub tool_lease_ms: u64,
753 pub speculate: Option<String>,
754 pub speculate_auto: bool,
755 pub speculate_fallbacks: Vec<String>,
756 pub speculation: SpeculationOptions,
757 pub trace_scope: otlp::TraceScope,
758}
759
760impl Default for DaemonOptions {
761 fn default() -> DaemonOptions {
762 DaemonOptions {
763 max_lanes: 8,
764 park_dir: None,
765 pressure_high_pct: 85,
766 pressure_low_pct: 70,
767 pin_budget_pct: 50,
768 park_lossy: false,
769 park_budget_bytes: 20 << 30,
770 class_lanes: [0; 4],
771 tick_decode_budget: 0,
772 tick_target_ms: 0,
773 runtime_id: None,
774 prefill_budget: scheduler::PREFILL_BUDGET,
775 agent_starvation_ticks: 8,
776 pressure_source: pressure::PressureConfig::None,
777 tool_lease_ms: 0,
778 speculate: None,
779 speculate_auto: false,
780 speculate_fallbacks: Vec::new(),
781 speculation: SpeculationOptions::default(),
782 files_max_bytes: None,
783 files_expiry_secs: None,
784 media_dir: None,
785 completion_deadline_ms: 0,
786 trace_scope: otlp::TraceScope::default(),
787 completion_bucket: completion_bucket::BucketConfig::default(),
788 }
789 }
790}
791
792struct PurgeFence {
793 set: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
794 ids: Vec<u64>,
795}
796
797impl Drop for PurgeFence {
798 fn drop(&mut self) {
799 let mut p = self.set.lock().expect("purging");
800 for id in &self.ids {
801 p.remove(id);
802 }
803 }
804}
805
806pub(crate) fn render_refusal(
807 codec: &dyn TextCodec,
808 messages: &[codec::ChatMessage],
809 tools: &[String],
810 kwargs: &serde_json::Map<String, serde_json::Value>,
811 fallback: &'static str,
812) -> DaemonError {
813 match codec.template_refusal(messages, tools, kwargs) {
814 Some(why) => DaemonError::TemplateRefused(why),
815 None => DaemonError::Protocol(fallback),
816 }
817}
818
819impl Daemon {
820 pub fn new(
821 store: SessionStore,
822 host: EngineHost,
823 codec: Box<dyn TextCodec + Send + Sync>,
824 max_lanes: usize,
825 ) -> Daemon {
826 Daemon::with_park(store, host, codec, max_lanes, None)
827 }
828
829 pub fn with_park(
830 store: SessionStore,
831 host: EngineHost,
832 codec: Box<dyn TextCodec + Send + Sync>,
833 max_lanes: usize,
834 park_dir: Option<std::path::PathBuf>,
835 ) -> Daemon {
836 Daemon::with_options(
837 store,
838 host,
839 codec,
840 DaemonOptions {
841 max_lanes,
842 park_dir,
843 ..DaemonOptions::default()
844 },
845 )
846 .expect("default watermarks are valid")
847 }
848
849 pub fn with_options(
850 store: SessionStore,
851 host: EngineHost,
852 codec: Box<dyn TextCodec + Send + Sync>,
853 opts: DaemonOptions,
854 ) -> Result<Daemon, DaemonError> {
855 Self::assemble(store, opts, |ground, opts| Serving::start(host, std::sync::Arc::from(codec), ground, opts))
856 }
857
858 pub fn over_fleet(
861 store: SessionStore,
862 codec: Box<dyn TextCodec + Send + Sync>,
863 fleet: std::sync::Arc<fleet_manager::FleetManager>,
864 model: String,
865 opts: DaemonOptions,
866 ) -> Result<Daemon, DaemonError> {
867 let backing = (std::sync::Arc::clone(&fleet), model.clone());
868 let mut daemon =
869 Self::assemble(store, opts, |ground, opts| Ok(Serving::over_fleet(std::sync::Arc::from(codec), ground, fleet, model, opts)))?;
870 daemon.fleet = Some(backing);
871 Ok(daemon)
872 }
873
874 pub fn fleet_window(&self) -> Option<u64> {
877 self.fleet.as_ref().and_then(|(fleet, model)| fleet.advertised_context(model))
878 }
879
880 pub fn fleet_serving(&self) -> Option<(usize, usize)> {
882 self.fleet.as_ref().map(|(fleet, model)| fleet.serving(model))
883 }
884
885 pub fn fleet_nodes(&self) -> Option<(usize, usize)> {
887 self.fleet.as_ref().map(|(fleet, _)| fleet.nodes_up())
888 }
889
890 fn assemble(
891 store: SessionStore,
892 opts: DaemonOptions,
893 start: impl FnOnce(Ground<'_>, DaemonOptions) -> Result<Serving, DaemonError>,
894 ) -> Result<Daemon, DaemonError> {
895 if opts.pressure_high_pct == 0 || opts.pressure_high_pct > 100 {
896 return Err(DaemonError::Config(
897 "pressure_high_pct must be in 1..=100",
898 ));
899 }
900 if opts.pressure_low_pct >= opts.pressure_high_pct {
901 return Err(DaemonError::Config(
902 "pressure_low_pct must be below pressure_high_pct",
903 ));
904 }
905 if opts.max_lanes == 0 {
906 return Err(DaemonError::Config("max_lanes must be at least 1"));
907 }
908 let mut opts = opts;
909 let runtime_id = opts.runtime_id;
910 opts.pin_budget_pct = opts.pin_budget_pct.min(opts.pressure_low_pct);
911 opts.completion_bucket.validate().map_err(DaemonError::Config)?;
912 if matches!(opts.speculation.max_draft, Some(0 | 16..)) {
913 return Err(DaemonError::Config("speculation max_draft must be in 1..=15"));
914 }
915 if opts.speculation.min_yield.is_some_and(|v| !v.is_finite() || v < 0.0) {
916 return Err(DaemonError::Config("speculation min_yield must be finite and non-negative"));
917 }
918 if opts.speculation.yield_rounds == Some(0) {
919 return Err(DaemonError::Config("speculation yield_rounds must be at least 1"));
920 }
921 if opts.speculation.min_speedup.is_some_and(|v| !v.is_finite() || v < 1.0) {
922 return Err(DaemonError::Config(
923 "speculation min_speedup must be finite and at least 1.0",
924 ));
925 }
926 if opts.speculation.dspark_confidence.is_some_and(|v| !v.is_finite() || v <= 0.0) {
927 return Err(DaemonError::Config(
928 "speculation dspark_confidence must be finite and positive",
929 ));
930 }
931 opts.trace_scope.ns = export::store_trace_ns(&store.store_id());
932 let store = std::sync::Arc::new(std::sync::Mutex::new(store));
933 let built_with = opts.clone();
934 let cancels = std::sync::Arc::new(CancelSet::default());
935 let active = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
936 let purging = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
937 let stats = std::sync::Arc::new(scheduler::SchedStats::default());
938 let background_tick_divisor = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(1));
939 let park_dir = opts.park_dir.clone();
940 let tool_lease_ms = opts.tool_lease_ms;
941 let files_max_bytes = opts.files_max_bytes;
942 let files_expiry_secs = opts.files_expiry_secs;
943 let completion_deadline_ms = opts.completion_deadline_ms;
944 let completion_bucket = opts.completion_bucket;
945 let media_dir = opts.media_dir.clone().unwrap_or_else(|| {
946 park_dir
947 .as_ref()
948 .and_then(|p| p.parent().map(|d| d.join("media")))
949 .unwrap_or_else(|| {
950 let uniq = std::time::SystemTime::now()
951 .duration_since(std::time::UNIX_EPOCH)
952 .map(|d| d.as_nanos())
953 .unwrap_or(0);
954 std::env::temp_dir().join(format!("superfluid-{}-{}", std::process::id(), uniq)).join("media")
955 })
956 });
957 let trace_scope = opts.trace_scope.clone();
958 let serving = start(
959 Ground {
960 store: &store,
961 cancels: &cancels,
962 active: &active,
963 stats: &stats,
964 background_tick_divisor: &background_tick_divisor,
965 },
966 opts,
967 )?;
968 Ok(Daemon {
969 trace_scope,
970 serving: std::sync::RwLock::new(std::sync::Arc::new(serving)),
971 reloading: std::sync::Mutex::new(()),
972 reload_hook: std::sync::OnceLock::new(),
973 runtime_id,
974 store,
975 cancels,
976 active,
977 purging,
978 stats,
979 background_tick_divisor,
980 park_dir,
981 tool_lease_ms,
982 files: files::FileStore::with_limits(
983 files::files_dir_for(media_dir.parent().unwrap_or(media_dir.as_path())),
984 files_max_bytes,
985 files_expiry_secs,
986 ),
987 batches: std::sync::Arc::new(batches::BatchStore::new(batches::batches_dir_for(
988 media_dir.parent().unwrap_or(media_dir.as_path()),
989 ))),
990 responses: response_store::ResponseStore::new(response_store::responses_dir_for(
991 media_dir.parent().unwrap_or(media_dir.as_path()),
992 )),
993 media: media::MediaPool::new(media_dir),
994 completion: std::sync::Arc::new(CompletionSatellite {
995 store: std::sync::Mutex::new(SessionStore::ephemeral()),
996 cache: std::sync::Mutex::new(CompletionCache::default()),
997 deadline_ms: completion_deadline_ms,
998 cache_cap: 256,
999 bucket: completion_bucket,
1000 counters: CompletionCounters::default(),
1001 }),
1002 fim_satellite: std::sync::OnceLock::new(),
1003 built_with,
1004 fleet: None,
1005 })
1006 }
1007
1008 fn serving(&self) -> std::sync::Arc<Serving> {
1009 std::sync::Arc::clone(&self.serving.read().expect("serving"))
1010 }
1011
1012 fn codec(&self) -> std::sync::Arc<dyn TextCodec + Send + Sync> {
1013 std::sync::Arc::clone(&self.serving().codec)
1014 }
1015
1016 fn sched_tx(&self) -> std::sync::mpsc::Sender<scheduler::SchedCmd> {
1017 self.serving().sched.tx.clone()
1018 }
1019
1020 pub fn reload_with(
1021 &self,
1022 build: impl FnOnce() -> Result<(EngineHost, Box<dyn TextCodec + Send + Sync>), DaemonError>,
1023 ) -> Result<(), DaemonError> {
1024 let _one_at_a_time = self.reloading.lock().expect("reloading");
1025 self.shutdown();
1026 {
1027 let mut cancels = self.cancels.lock().expect("cancel registry");
1028 let active = self.active.lock().expect("active set");
1029 cancels.retain(|session| active.contains(session));
1030 }
1031 let started = build().and_then(|(host, codec)| {
1032 Serving::start(
1033 host,
1034 std::sync::Arc::from(codec),
1035 Ground {
1036 store: &self.store,
1037 cancels: &self.cancels,
1038 active: &self.active,
1039 stats: &self.stats,
1040 background_tick_divisor: &self.background_tick_divisor,
1041 },
1042 self.built_with.clone(),
1043 )
1044 });
1045 match started {
1046 Ok(serving) => {
1047 *self.serving.write().expect("serving") = std::sync::Arc::new(serving);
1048 Ok(())
1049 }
1050 Err(e) => {
1051 let stopped = self.serving();
1052 let mut why = stopped.retired.lock().expect("retired");
1053 if why.is_none() {
1054 *why = Some(format!("the model's engine was stopped to load it again, which failed ({e}); it loads again on its next request, so retry"));
1055 }
1056 Err(e)
1057 }
1058 }
1059 }
1060
1061 pub fn on_retired(&self, reload: Box<dyn Fn() + Send + Sync>) {
1062 let _ = self.reload_hook.set(reload);
1063 }
1064
1065 fn ensure_serving(&self) -> Result<(), DaemonError> {
1066 if self.needs_reload().is_none() {
1067 return Ok(());
1068 }
1069 if let Some(reload) = self.reload_hook.get() {
1070 reload();
1071 }
1072 match self.needs_reload() {
1073 None => Ok(()),
1074 Some(why) => Err(DaemonError::Generation(why)),
1075 }
1076 }
1077
1078 pub fn max_stream_tokens(&self) -> u64 {
1079 if let Some((fleet, model)) = &self.fleet {
1080 return fleet.advertised_context(model).unwrap_or(0);
1081 }
1082 let serving = self.serving();
1083 let record = serving.capabilities.read().expect("capability record");
1084 runtime::stream_ceiling(serving.staged_tokens, &record)
1085 }
1086
1087 fn check_stream_fits(&self, len: usize) -> Result<(), DaemonError> {
1088 let len = len as u64;
1089 let max = self.max_stream_tokens();
1090 if max > 0 && len > max {
1091 return Err(DaemonError::StreamTooLong { len, max });
1092 }
1093 Ok(())
1094 }
1095
1096 pub fn session_fits(&self, session: u64, thinking: Option<bool>) -> Result<(), DaemonError> {
1097 self.ensure_serving()?;
1098 let (len, turn) = {
1099 let store = self.store.lock().expect("store");
1100 let s = store.session(session)?;
1101 (s.tokens.len(), Self::opener_turn(&s.events))
1102 };
1103 self.check_stream_fits(len + self.generation_opener(turn, thinking).map_or(0, |op| op.len()))
1104 }
1105
1106 pub fn fim_fits(&self, req: &FimRequest) -> Result<(), DaemonError> {
1107 let d = match self.fim_satellite.get() {
1108 Some(sat) => &sat.daemon,
1109 None => self,
1110 };
1111 d.ensure_serving()?;
1112 let fim = d
1113 .codec()
1114 .render_fim(&req.prefix, &req.suffix, req.mode)
1115 .ok_or(DaemonError::NoFimDialect)?;
1116 d.check_stream_fits(fim.len())
1117 }
1118
1119 pub fn complete(
1120 &self,
1121 prefix: &str,
1122 suffix: &str,
1123 mode: u8,
1124 max_tokens: u32,
1125 ) -> Result<Completion, DaemonError> {
1126 self.complete_streaming(&FimRequest::greedy(prefix, suffix, mode, max_tokens), |_, _| {
1127 std::ops::ControlFlow::Continue(())
1128 })
1129 }
1130
1131 pub fn complete_streaming(
1132 &self,
1133 req: &FimRequest,
1134 on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1135 ) -> Result<Completion, DaemonError> {
1136 let result = match self.fim_satellite.get() {
1137 Some(sat) => sat.daemon.complete_here(req, on_text),
1138 None => self.complete_here(req, on_text),
1139 };
1140 if let Ok(c) = &result {
1141 use std::sync::atomic::Ordering::Relaxed;
1142 let n = &self.completion.counters;
1143 if c.expired {
1144 n.expired.fetch_add(1, Relaxed);
1145 } else if c.cached {
1146 n.cached.fetch_add(1, Relaxed);
1147 } else {
1148 n.generated.fetch_add(1, Relaxed);
1149 }
1150 }
1151 result
1152 }
1153
1154 pub fn attach_fim_satellite(
1155 &self,
1156 name: impl Into<String>,
1157 satellite: std::sync::Arc<Daemon>,
1158 ) -> Result<(), DaemonError> {
1159 if satellite.codec().render_fim("", "", codec::fim_mode::PSM).is_none() {
1160 return Err(DaemonError::NoFimDialect);
1161 }
1162 if satellite.fim_satellite.get().is_some() {
1163 return Err(DaemonError::Config("a FIM satellite cannot itself route to another"));
1164 }
1165 self.fim_satellite
1166 .set(FimSatellite {
1167 name: name.into(),
1168 daemon: satellite,
1169 })
1170 .map_err(|_| DaemonError::Config("a FIM satellite is already attached"))
1171 }
1172
1173 pub fn fim_satellite_name(&self) -> Option<String> {
1174 self.fim_satellite.get().map(|s| s.name.clone())
1175 }
1176
1177 pub fn fim_satellite(&self) -> Option<std::sync::Arc<Daemon>> {
1178 self.fim_satellite.get().map(|s| std::sync::Arc::clone(&s.daemon))
1179 }
1180
1181 pub fn fim_supported(&self) -> bool {
1182 self.fim_satellite.get().is_some()
1183 || self.codec().render_fim("", "", codec::fim_mode::PSM).is_some()
1184 }
1185
1186 pub fn completion_bucket(&self) -> completion_bucket::BucketConfig {
1187 self.completion.bucket
1188 }
1189
1190 pub fn note_completion_throttled(&self) {
1191 self.completion
1192 .counters
1193 .throttled
1194 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1195 }
1196
1197 fn complete_here(
1198 &self,
1199 req: &FimRequest,
1200 mut on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1201 ) -> Result<Completion, DaemonError> {
1202 self.ensure_serving()?;
1203 let _span = self.raw_generate_span(req.max_tokens);
1204 let submitted = std::time::Instant::now();
1205 let fim = self
1206 .codec()
1207 .render_fim(&req.prefix, &req.suffix, req.mode)
1208 .ok_or(DaemonError::NoFimDialect)?;
1209 self.check_stream_fits(fim.len())?;
1210 let prompt_tokens = fim.len() as u32;
1211 let expired = || Completion {
1212 text: String::new(),
1213 tokens: Vec::new(),
1214 cached: false,
1215 expired: true,
1216 finish: superfluid_abi::finish::NONE,
1217 prompt_tokens,
1218 };
1219 let cacheable = req.params.temperature <= 0.0;
1220 let key = {
1221 let mut h = superfluid_fingerprint::RecordHasher::new();
1222 for t in &fim {
1223 h.field_u32(1, *t);
1224 }
1225 h.field_u32(2, req.mode as u32);
1226 h.field_u32(3, req.max_tokens);
1227 if let Some(fp) = self.codec().behavior_fingerprint() {
1228 h.field_u64(4, fp.digest());
1229 }
1230 let x = &req.extras;
1231 h.field_u32(5, x.presence_penalty.to_bits());
1232 h.field_u32(6, x.frequency_penalty.to_bits());
1233 h.field_u32(7, x.repeat_penalty.to_bits());
1234 h.field_u32(8, x.ignore_eos as u32);
1235 h.finish()
1236 };
1237 if cacheable {
1238 if let Some((tokens, text, finish)) = self.completion.cache_get(key) {
1239 let _ = on_text(&text, tokens.len());
1240 return Ok(Completion {
1241 text,
1242 tokens,
1243 cached: true,
1244 expired: false,
1245 finish,
1246 prompt_tokens,
1247 });
1248 }
1249 }
1250 if self.completion.deadline_ms > 0
1251 && submitted.elapsed().as_millis() as u64 > self.completion.deadline_ms
1252 {
1253 return Ok(expired());
1254 }
1255 let session = {
1256 let mut store = self.completion.store.lock().expect("completion store");
1257 let s = store.create(None, req.params)?;
1258 store.set_qos(s, qos::INLINE_COMPLETION, false)?;
1259 store.append(s, None, fim.clone())?;
1260 s
1261 };
1262 let (stream, params) = {
1263 let store = self.completion.store.lock().expect("completion store");
1264 let st = store.session(session)?;
1265 (st.tokens.clone(), st.params)
1266 };
1267 let mut produced: Vec<u32> = Vec::new();
1268 let mut emitted = 0usize;
1269 let result = scheduler::run_completion_streaming(
1270 &self.sched_tx(),
1271 &self.cancels,
1272 stream,
1273 params,
1274 req.max_tokens,
1275 self.completion.deadline_ms,
1276 scheduler::GenExtras {
1277 grammar_handle: 0,
1278 logit_bias_handle: 0,
1279 want_logprobs: false,
1280 ..req.extras.clone()
1281 },
1282 |toks| {
1283 produced.extend_from_slice(toks);
1284 let text = self.codec().decode(&produced);
1285 let stable = text.trim_end_matches('\u{FFFD}');
1286 if stable.len() > emitted && stable.is_char_boundary(emitted) {
1287 let piece = &stable[emitted..];
1288 emitted = stable.len();
1289 return on_text(piece, produced.len());
1290 }
1291 std::ops::ControlFlow::Continue(())
1292 },
1293 );
1294 {
1295 let mut store = self.completion.store.lock().expect("completion store");
1296 let _ = store.drop_session(session);
1297 }
1298 let (tokens, finish, was_expired) = result?;
1299 if was_expired {
1300 return Ok(expired());
1301 }
1302 let text = self.codec().decode(&tokens);
1303 if text.len() > emitted && text.is_char_boundary(emitted) {
1304 let _ = on_text(&text[emitted..], tokens.len());
1305 }
1306 if cacheable && finish != superfluid_abi::finish::CANCELLED {
1307 self.completion.cache_put(key, tokens.clone(), text.clone(), finish);
1308 }
1309 Ok(Completion {
1310 text,
1311 tokens,
1312 cached: false,
1313 expired: false,
1314 finish,
1315 prompt_tokens,
1316 })
1317 }
1318
1319 pub fn generate_tokens(
1320 &self,
1321 stream: Vec<u32>,
1322 params: GenParams,
1323 max_tokens: u32,
1324 deadline_ms: u64,
1325 ) -> Result<(Vec<u32>, bool), DaemonError> {
1326 self.ensure_serving()?;
1327 let _span = self.raw_generate_span(max_tokens);
1328 self.check_stream_fits(stream.len())?;
1329 let deadline = self.effective_deadline(deadline_ms);
1330 let (tokens, _finish, expired) =
1331 scheduler::run_completion(&self.sched_tx(), &self.cancels, stream, params, max_tokens, deadline, 0)?;
1332 Ok((tokens, expired))
1333 }
1334
1335 pub fn generate_tokens_streaming(
1336 &self,
1337 stream: Vec<u32>,
1338 params: GenParams,
1339 max_tokens: u32,
1340 deadline_ms: u64,
1341 extras: scheduler::GenExtras,
1342 on_tokens: impl FnMut(&[u32]) -> std::ops::ControlFlow<()>,
1343 ) -> Result<(Vec<u32>, u32, bool), DaemonError> {
1344 self.generate_tokens_streaming_lp(stream, params, max_tokens, deadline_ms, qos::INLINE_COMPLETION, extras, Vec::new(), on_tokens, |_| {})
1345 }
1346
1347 #[allow(clippy::too_many_arguments)]
1350 pub fn generate_tokens_streaming_lp(
1351 &self,
1352 stream: Vec<u32>,
1353 params: GenParams,
1354 max_tokens: u32,
1355 deadline_ms: u64,
1356 class: u8,
1357 extras: scheduler::GenExtras,
1358 media: Vec<(u64, std::path::PathBuf, u32)>,
1359 on_tokens: impl FnMut(&[u32]) -> std::ops::ControlFlow<()>,
1360 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
1361 ) -> Result<(Vec<u32>, u32, bool), DaemonError> {
1362 self.ensure_serving()?;
1363 let _span = self.raw_generate_span(max_tokens);
1364 self.check_stream_fits(stream.len())?;
1365 let deadline = self.effective_deadline(deadline_ms);
1366 scheduler::run_completion_streaming_lp(
1367 &self.sched_tx(),
1368 &self.cancels,
1369 stream,
1370 params,
1371 max_tokens,
1372 deadline,
1373 class,
1374 extras,
1375 media,
1376 on_tokens,
1377 on_logprobs,
1378 )
1379 }
1380
1381 pub fn media_put(&self, bytes: &[u8]) -> std::io::Result<String> {
1383 self.media.put(bytes)
1384 }
1385
1386 pub fn media_path(&self, hash: &str) -> std::path::PathBuf {
1388 self.media.path_of(hash)
1389 }
1390
1391 pub fn state_sync(&self, req: proto_linkw::StateSyncReqMsg) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1393 self.ensure_serving()?;
1394 self.engine_sync(req)
1395 }
1396
1397 pub fn state_sync_stream(
1400 &self,
1401 req: proto_linkw::StateSyncReqMsg,
1402 segments: std::sync::mpsc::Sender<proto_linkw::TranscribeSegmentMsg>,
1403 ) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1404 self.ensure_serving()?;
1405 let (tx, rx) = std::sync::mpsc::channel();
1406 self.sched_tx()
1407 .send(scheduler::SchedCmd::SyncStream(req, segments, tx))
1408 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1409 match rx.recv() {
1410 Ok(Ok(ok)) => Ok(ok),
1411 Ok(Err(e)) => Err(DaemonError::Generation(e)),
1412 Err(_) => Err(DaemonError::Protocol("scheduler dropped the query")),
1413 }
1414 }
1415
1416 pub fn forward_stage(
1417 &self,
1418 tokens: Vec<u32>,
1419 start_layer: u32,
1420 end_layer: u32,
1421 hidden_in: Option<Vec<u8>>,
1422 ) -> Result<superfluid_engine::StageOutput, DaemonError> {
1423 self.ensure_serving()?;
1424 match self.engine_sync(proto_linkw::StateSyncReqMsg::ForwardStage {
1425 tokens,
1426 start_layer,
1427 end_layer,
1428 hidden_in,
1429 })? {
1430 proto_linkw::StateSyncOkMsg::ForwardStage {
1431 hidden: Some(h),
1432 token: None,
1433 } => Ok(superfluid_engine::StageOutput::Hidden(h)),
1434 proto_linkw::StateSyncOkMsg::ForwardStage {
1435 hidden: None,
1436 token: Some(t),
1437 } => Ok(superfluid_engine::StageOutput::Token(t)),
1438 _ => Err(DaemonError::Protocol("mismatched ForwardStage reply")),
1439 }
1440 }
1441
1442 fn effective_deadline(&self, deadline_ms: u64) -> u64 {
1443 if deadline_ms == 0 {
1444 self.completion.deadline_ms
1445 } else if self.completion.deadline_ms == 0 {
1446 deadline_ms
1447 } else {
1448 deadline_ms.min(self.completion.deadline_ms)
1449 }
1450 }
1451
1452 pub fn generate_tokens_constrained(
1453 &self,
1454 stream: Vec<u32>,
1455 params: GenParams,
1456 max_tokens: u32,
1457 deadline_ms: u64,
1458 json_schema: &str,
1459 ) -> Result<(Vec<u32>, bool), DaemonError> {
1460 self.ensure_serving()?;
1461 let _span = self.raw_generate_span(max_tokens);
1462 let handle = match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1463 json_schema: json_schema.to_string(),
1464 })? {
1465 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => grammar_handle,
1466 _ => return Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1467 };
1468 let deadline = self.effective_deadline(deadline_ms);
1469 let result =
1470 scheduler::run_completion(&self.sched_tx(), &self.cancels, stream, params, max_tokens, deadline, handle);
1471 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar {
1472 grammar_handle: handle,
1473 });
1474 let (tokens, finish, expired) = result?;
1475 if expired {
1476 return Err(DaemonError::Generation(format!(
1477 "constrained generation hit the wall deadline with the grammar incomplete ({} tokens)",
1478 tokens.len()
1479 )));
1480 }
1481 if finish != superfluid_abi::finish::GRAMMAR {
1482 return Err(DaemonError::Generation(format!(
1483 "constrained generation did not complete the grammar (finish={finish}, {} tokens); raise max_tokens",
1484 tokens.len()
1485 )));
1486 }
1487 Ok((tokens, false))
1488 }
1489
1490 pub fn put_media(&self, bytes: &[u8]) -> Result<String, DaemonError> {
1491 Ok(self.media.put(bytes)?)
1492 }
1493
1494 pub fn media_pool(&self) -> &media::MediaPool {
1495 &self.media
1496 }
1497
1498 fn engine_sync(&self, req: proto_linkw::StateSyncReqMsg) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1499 self.engine_sync_as(req, None)
1500 }
1501
1502 fn engine_sync_as(
1503 &self,
1504 req: proto_linkw::StateSyncReqMsg,
1505 class: Option<u8>,
1506 ) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1507 let class = class.or_else(ambient_sync_class);
1508 let (tx, rx) = std::sync::mpsc::channel();
1509 let cmd = scheduler::SchedCmd::Sync(req, tx);
1510 self.sched_tx()
1511 .send(match class {
1512 Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1513 None => cmd,
1514 })
1515 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1516 rx.recv()
1517 .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))?
1518 .map_err(DaemonError::Generation)
1519 }
1520
1521 pub fn append_image(
1522 &self,
1523 session: u64,
1524 role: u32,
1525 blob: &str,
1526 pre_text: &str,
1527 post_text: &str,
1528 ) -> Result<CommittedEvent, DaemonError> {
1529 if self.active.lock().expect("active set").contains(&session) {
1530 return Err(DaemonError::SessionBusy(session));
1531 }
1532 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
1533 return Err(DaemonError::UnknownRole(role));
1534 }
1535 if !self.media.exists(blob) {
1536 return Err(DaemonError::UnknownMedia(blob.to_string()));
1537 }
1538 let path = self.media.path_of(blob);
1539 let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
1540 image_path: path.to_string_lossy().into_owned(),
1541 })? {
1542 proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
1543 _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
1544 };
1545 if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
1546 return Err(DaemonError::MediaTooLarge(info.n_tokens));
1547 }
1548 let mut run = Vec::with_capacity(info.n_tokens as usize + 2);
1549 if info.boi_token_id != 0 {
1550 run.push(info.boi_token_id);
1551 }
1552 run.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
1553 if info.eoi_token_id != 0 {
1554 run.push(info.eoi_token_id);
1555 }
1556 let mut span = self.separator_for(session);
1557 let turn = self
1558 .codec()
1559 .render_turn_with_tokens(role, pre_text, &run, post_text)
1560 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
1561 span.extend(turn);
1562 let first_in_span = span
1563 .iter()
1564 .position(|&t| t == info.image_token_id)
1565 .ok_or(DaemonError::Protocol("codec dropped the placeholder run"))?;
1566 let mut store = self.store.lock().expect("store");
1567 let stream_len = store.session(session)?.tokens.len() as u64;
1568 let payload = serde_json::json!({
1569 "blob": blob,
1570 "n_tokens": info.n_tokens,
1571 "image_token_id": info.image_token_id,
1572 "offset": stream_len + first_in_span as u64,
1573 "pre_text": pre_text,
1574 "post_text": post_text,
1575 "media_identity": {
1576 "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
1577 "encoder": "bundle-weights-identity",
1578 },
1579 })
1580 .to_string();
1581 store.commit_block(
1582 session,
1583 role,
1584 wal::block_kind::IMAGE,
1585 payload,
1586 span,
1587 codec::MODEL_VISIBILITY_VERSION,
1588 )
1589 }
1590
1591 fn media_of(&self, s: &store::SessionState) -> Vec<(u64, std::path::PathBuf, u32)> {
1592 let mut v = Vec::new();
1593 for e in &s.events {
1594 if let EventBody::Block {
1595 kind, payload, ..
1596 } = &e.body
1597 {
1598 if *kind != wal::block_kind::IMAGE {
1599 continue;
1600 }
1601 let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1602 let blob = p.get("blob").and_then(|b| b.as_str()).unwrap_or("");
1603 let off = p.get("offset").and_then(|o| o.as_u64()).unwrap_or(0);
1604 let n = p.get("n_tokens").and_then(|o| o.as_u64()).unwrap_or(0) as u32;
1605 if !blob.is_empty() && n > 0 {
1606 v.push((off, self.media.path_of(blob), n));
1607 }
1608 }
1609 }
1610 v
1611 }
1612
1613 pub fn gc_media(&self) -> Result<(usize, usize), DaemonError> {
1614 let store = self.store.lock().expect("store");
1615 let mut live = std::collections::HashSet::new();
1616 for id in store.session_ids() {
1617 let Ok(s) = store.session(id) else { continue };
1618 for e in &s.events {
1619 if let EventBody::Block { kind, payload, .. } = &e.body {
1620 if *kind == wal::block_kind::IMAGE {
1621 let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1622 if let Some(b) = p.get("blob").and_then(|b| b.as_str()) {
1623 live.insert(b.to_string());
1624 }
1625 }
1626 }
1627 }
1628 }
1629 Ok(self.media.sweep(&live)?)
1630 }
1631
1632 pub fn speculation(&self) -> Option<scheduler::SpeculationGrant> {
1633 self.serving().speculation.grant()
1634 }
1635
1636 pub fn tool_lease_ms(&self) -> u64 {
1637 self.tool_lease_ms
1638 }
1639
1640 pub fn inspect(&self, session: u64) -> Result<Inspection, DaemonError> {
1641 let resident = self.active.lock().expect("active set").contains(&session);
1643 let store = self.store.lock().expect("store");
1644 let s = store.session(session)?;
1645 let tier = if resident {
1646 Tier::Resident
1647 } else {
1648 match self
1649 .park_dir
1650 .as_deref()
1651 .and_then(|d| park::read(d, session))
1652 {
1653 Some(p) => Tier::Parked {
1654 covered: p.covered,
1655 encoding: p.encoding,
1656 },
1657 None => Tier::Cold,
1658 }
1659 };
1660 Ok(Inspection {
1661 summary: SessionSummary {
1662 id: s.id,
1663 parent: if s.rerooted { None } else { s.parent },
1664 fork_at: s.base,
1665 title: s.title.clone(),
1666 archived: s.archived,
1667 meta_version: s.meta_version,
1668 generation: s.generation,
1669 events: s.events.len() as u64,
1670 tokens: s.tokens.len() as u64,
1671 open_tool_calls: s.open_tool_calls.len() as u64,
1672 },
1673 params: s.params,
1674 epoch: s.epoch,
1675 last_finish: s.last_finish,
1676 qos_class: s.qos_class,
1677 batch_invariant: s.batch_invariant,
1678 pin_deadline_unix_ms: s.pin_deadline_unix_ms,
1679 behavior_fingerprint: s.behavior_fp,
1680 open_tool_calls: s
1681 .open_tool_calls
1682 .iter()
1683 .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
1684 .collect(),
1685 tier,
1686 tokens: s.tokens.clone(),
1687 events: s.events.iter().cloned().map(Into::into).collect(),
1688 wal_version: store.wal_version(),
1689 store_id: Some(store.store_id()),
1690 })
1691 }
1692
1693 pub fn files(&self) -> &files::FileStore {
1694 &self.files
1695 }
1696
1697 pub fn batches(&self) -> std::sync::Arc<batches::BatchStore> {
1698 std::sync::Arc::clone(&self.batches)
1699 }
1700
1701 pub fn responses(&self) -> &response_store::ResponseStore {
1702 &self.responses
1703 }
1704
1705 pub fn tokenize(&self, text: &str) -> Vec<u32> {
1706 self.codec().encode(text)
1707 }
1708
1709 pub fn grammar_create(&self, json_schema: &str) -> Result<u32, DaemonError> {
1710 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1711 json_schema: json_schema.to_string(),
1712 })? {
1713 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
1714 _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1715 }
1716 }
1717
1718 pub fn grammar_free(&self, grammar_handle: u32) {
1719 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar { grammar_handle });
1720 }
1721
1722 pub fn logit_bias_create(&self, tokens: &[i32], values: &[f32]) -> Result<u32, DaemonError> {
1723 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateLogitBias {
1724 tokens: tokens.to_vec(),
1725 values: values.iter().map(|v| v.to_bits()).collect(),
1726 })? {
1727 proto_linkw::StateSyncOkMsg::CreateLogitBias { handle } => Ok(handle),
1728 _ => Err(DaemonError::Protocol("mismatched CreateLogitBias reply")),
1729 }
1730 }
1731 pub fn logit_bias_free(&self, handle: u32) {
1732 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeLogitBias { handle });
1733 }
1734
1735 pub fn lora_load(&self, adapter_path: &str) -> Result<Option<String>, DaemonError> {
1736 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraLoad {
1737 adapter_path: adapter_path.to_string(),
1738 })
1739 }
1740
1741 pub fn lora_unload(&self) -> Result<Option<String>, DaemonError> {
1742 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraUnload)
1743 }
1744
1745 pub fn lora_id(&self) -> Result<Option<String>, DaemonError> {
1746 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraId)
1747 }
1748
1749 fn lora_reply(&self, req: proto_linkw::StateSyncReqMsg) -> Result<Option<String>, DaemonError> {
1750 match self.engine_sync(req)? {
1751 proto_linkw::StateSyncOkMsg::LoraId { id } => Ok(id),
1752 _ => Err(DaemonError::Protocol("mismatched LoraId reply")),
1753 }
1754 }
1755
1756 pub fn try_model_sampling_defaults(&self) -> Option<ModelSamplingDefaults> {
1757 let serving = self.serving();
1758 if let Some(cached) = serving.model_sampling.get() {
1759 return Some(cached.clone());
1760 }
1761 let descriptor = self.capability_descriptor()?;
1762 let resolved = descriptor
1763 .get("sampling_defaults")
1764 .map(ModelSamplingDefaults::from_json)
1765 .unwrap_or_default();
1766 let _ = serving.model_sampling.set(resolved.clone());
1767 Some(resolved)
1768 }
1769
1770 pub fn model_sampling_defaults(&self) -> ModelSamplingDefaults {
1771 self.try_model_sampling_defaults().unwrap_or_default()
1772 }
1773
1774 pub fn capability_descriptor(&self) -> Option<serde_json::Value> {
1775 self.serving().capabilities.read().expect("capability record").with_runtime(self.runtime_id)
1776 }
1777
1778 pub fn runtime_id(&self) -> Option<&'static str> {
1779 self.runtime_id
1780 }
1781
1782 pub fn needs_reload(&self) -> Option<String> {
1783 self.serving().retired.lock().expect("retired").clone()
1784 }
1785
1786 pub fn capabilities(&self) -> capabilities::Capabilities {
1787 self.serving().capabilities.read().expect("capability record").clone()
1788 }
1789
1790 pub fn require(&self, section: &str, key: &str, code: &'static str, param: Option<&str>, what: &str) -> Result<(), DaemonError> {
1791 let why = match self.serving().capabilities.read().expect("capability record").get(section, key) {
1792 capabilities::Cap::No(why) => why.to_string(),
1793 capabilities::Cap::Yes | capabilities::Cap::Unknown => return Ok(()),
1794 };
1795 Err(DaemonError::Unsupported(Refusal::new(code, param, format!("{} {what}: {why}", self.described()))))
1796 }
1797
1798 fn described(&self) -> String {
1799 let model = &self.trace_scope.model;
1800 let runtime = self.capability_descriptor().and_then(|r| r["runtime"]["id"].as_str().map(str::to_string));
1801 match (model.is_empty(), runtime) {
1802 (false, Some(r)) => format!("model '{model}' on runtime {r}"),
1803 (false, None) => format!("model '{model}'"),
1804 (true, Some(r)) => format!("this model (runtime {r})"),
1805 (true, None) => "this model".to_string(),
1806 }
1807 }
1808
1809 pub fn chat_template(&self) -> Option<String> {
1810 self.codec().chat_template_source()
1811 }
1812
1813 pub fn transcribe(
1814 &self,
1815 audio_path: &str,
1816 params: &superfluid_engine::TranscribeParams,
1817 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1818 self.transcribe_as(audio_path, params, None)
1819 }
1820
1821 pub fn transcribe_as(
1822 &self,
1823 audio_path: &str,
1824 params: &superfluid_engine::TranscribeParams,
1825 class: Option<u8>,
1826 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1827 self.ensure_serving()?;
1828 self.require_transcription(params)?;
1829 Self::decode_transcription(self.engine_sync_as(Self::transcribe_req(audio_path, params, false), class)?)
1830 }
1831
1832 fn require_transcription(&self, params: &superfluid_engine::TranscribeParams) -> Result<(), DaemonError> {
1833 if params.translate {
1834 self.require("modalities", "whisper_translate", "unsupported_model", Some("model"), "does not translate speech")
1835 } else {
1836 self.require("modalities", "whisper_transcribe", "unsupported_model", Some("model"), "does not transcribe speech")
1837 }
1838 }
1839
1840 #[allow(clippy::type_complexity)]
1841 pub fn transcribe_streaming(
1842 &self,
1843 audio_path: &str,
1844 params: &superfluid_engine::TranscribeParams,
1845 ) -> Result<
1846 (
1847 std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1848 std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1849 ),
1850 DaemonError,
1851 > {
1852 self.transcribe_streaming_as(audio_path, params, None)
1853 }
1854
1855 #[allow(clippy::type_complexity)]
1856 pub fn transcribe_streaming_as(
1857 &self,
1858 audio_path: &str,
1859 params: &superfluid_engine::TranscribeParams,
1860 class: Option<u8>,
1861 ) -> Result<
1862 (
1863 std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1864 std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1865 ),
1866 DaemonError,
1867 > {
1868 self.ensure_serving()?;
1869 self.require_transcription(params)?;
1870 let (seg_tx, seg_rx) = std::sync::mpsc::channel();
1871 let (res_tx, res_rx) = std::sync::mpsc::channel();
1872 let (raw_tx, raw_rx) = std::sync::mpsc::channel();
1873 self.sched_tx()
1874 .send({
1875 let cmd = scheduler::SchedCmd::SyncStream(Self::transcribe_req(audio_path, params, true), seg_tx, raw_tx);
1876 match class.or_else(ambient_sync_class) {
1877 Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1878 None => cmd,
1879 }
1880 })
1881 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1882 std::thread::spawn(move || {
1883 let out = match raw_rx.recv() {
1884 Ok(Ok(ok)) => Self::decode_transcription(ok),
1885 Ok(Err(e)) => Err(DaemonError::Generation(e)),
1886 Err(_) => Err(DaemonError::Protocol("scheduler dropped the query")),
1887 };
1888 let _ = res_tx.send(out);
1889 });
1890 Ok((seg_rx, res_rx))
1891 }
1892
1893 fn transcribe_req(
1894 audio_path: &str,
1895 params: &superfluid_engine::TranscribeParams,
1896 stream: bool,
1897 ) -> proto_linkw::StateSyncReqMsg {
1898 proto_linkw::StateSyncReqMsg::Transcribe {
1899 audio_path: audio_path.to_string(),
1900 language: params.language.clone(),
1901 translate: params.translate,
1902 timestamps: params.timestamps,
1903 prompt: params.prompt.clone(),
1904 stream,
1905 }
1906 }
1907
1908 fn decode_transcription(
1909 ok: proto_linkw::StateSyncOkMsg,
1910 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1911 match ok {
1912 proto_linkw::StateSyncOkMsg::Transcribe { text, language, duration_ms, segments } => {
1913 Ok(superfluid_engine::Transcription {
1914 text,
1915 language,
1916 duration_ms,
1917 segments: segments
1918 .into_iter()
1919 .map(|s| superfluid_engine::TranscriptSegment {
1920 start_ms: s.start_ms,
1921 end_ms: s.end_ms,
1922 text: s.text,
1923 avg_logprob: f32::from_bits(s.avg_logprob_bits),
1924 no_speech_prob: f32::from_bits(s.no_speech_prob_bits),
1925 compression_ratio: f32::from_bits(s.compression_ratio_bits),
1926 temperature: f32::from_bits(s.temperature_bits),
1927 })
1928 .collect(),
1929 })
1930 }
1931 _ => Err(DaemonError::Protocol("engine returned a non-Transcribe reply")),
1932 }
1933 }
1934
1935 pub fn embed(&self, text: &str) -> Result<Vec<f32>, DaemonError> {
1936 self.embed_as(text, None)
1937 }
1938
1939 pub fn embed_as(&self, text: &str, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1940 self.ensure_serving()?;
1941 self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1942 self.embed_tokens_as(self.codec().encode(text), class)
1943 }
1944
1945 pub(crate) fn embed_tokens_as(&self, tokens: Vec<u32>, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1946 self.ensure_serving()?;
1947 self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1948 match self.engine_sync_as(proto_linkw::StateSyncReqMsg::Embed { tokens }, class)? {
1949 proto_linkw::StateSyncOkMsg::Embed { embedding } => Ok(embedding
1950 .as_chunks::<4>()
1951 .0
1952 .iter()
1953 .map(|b| f32::from_le_bytes(*b))
1954 .collect()),
1955 _ => Err(DaemonError::Protocol("engine returned a non-Embed reply")),
1956 }
1957 }
1958
1959 pub fn detokenize(&self, tokens: &[u32]) -> String {
1960 self.codec().decode(tokens)
1961 }
1962
1963 fn raw_generate_span(&self, max_tokens: u32) -> tracing::span::EnteredSpan {
1964 tracing::info_span!(
1965 parent: None,
1966 "generate",
1967 max_tokens,
1968 raw = true,
1969 model = self.trace_scope.model.as_str(),
1970 store_ns = self.trace_scope.ns
1971 )
1972 .entered()
1973 }
1974
1975 pub fn metrics_text(&self) -> String {
1976 let mut out = metrics::render_prometheus(&self.stats);
1977 if let Some((fleet, model)) = &self.fleet {
1978 out.push_str(&fleet_metrics::fleet_metrics(&fleet.status(), &metrics::hostname(), model));
1979 }
1980 out.push_str(&format!(
1981 "# HELP superfluid_telemetry_dropped_total Telemetry records dropped by the bounded sinks (log lines and OTLP spans).\n# TYPE superfluid_telemetry_dropped_total counter\nsuperfluid_telemetry_dropped_total {}\n",
1982 telemetry::dropped_total()
1983 ));
1984 let g = |a: &std::sync::atomic::AtomicU64| a.load(std::sync::atomic::Ordering::Relaxed);
1985 out.push_str(&format!(
1986 "# HELP superfluid_otlp_spans_exported_total Operational spans accepted by the OTLP collector.\n# TYPE superfluid_otlp_spans_exported_total counter\nsuperfluid_otlp_spans_exported_total {}\n",
1987 g(&otlp::EXPORTED)
1988 ));
1989 out.push_str(&format!(
1990 "# HELP superfluid_otlp_spans_dropped_total Operational spans dropped instead of delaying a tick.\n# TYPE superfluid_otlp_spans_dropped_total counter\nsuperfluid_otlp_spans_dropped_total{{reason=\"queue_full\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"export_failed\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"rejected\"}} {}\n",
1991 g(&otlp::DROPPED_QUEUE_FULL),
1992 g(&otlp::DROPPED_EXPORT_FAILED),
1993 g(&otlp::DROPPED_REJECTED)
1994 ));
1995 out.push_str(&self.completion_metrics_text());
1996 out
1997 }
1998
1999 fn completion_metrics_text(&self) -> String {
2000 use std::sync::atomic::Ordering::Relaxed;
2001 let n = &self.completion.counters;
2002 let mut out = String::from(
2003 "# HELP superfluid_fim_completions_total FIM completions by outcome.\n\
2004 # TYPE superfluid_fim_completions_total counter\n",
2005 );
2006 for (label, v) in [
2007 ("generated", &n.generated),
2008 ("cached", &n.cached),
2009 ("expired", &n.expired),
2010 ("throttled", &n.throttled),
2011 ] {
2012 out.push_str(&format!(
2013 "superfluid_fim_completions_total{{result=\"{label}\"}} {}\n",
2014 v.load(Relaxed)
2015 ));
2016 }
2017 if let Some(sat) = self.fim_satellite.get() {
2018 let s = &sat.daemon.stats;
2019 let model = sat.name.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
2020 out.push_str(&format!(
2021 "# HELP superfluid_fim_satellite_info The --fim-model satellite serving completions.\n\
2022 # TYPE superfluid_fim_satellite_info gauge\n\
2023 superfluid_fim_satellite_info{{model=\"{model}\"}} 1\n\
2024 # HELP superfluid_fim_satellite_decode_tokens_total Tokens the satellite decoded.\n\
2025 # TYPE superfluid_fim_satellite_decode_tokens_total counter\n\
2026 superfluid_fim_satellite_decode_tokens_total {}\n\
2027 # HELP superfluid_fim_satellite_lanes_active Satellite lanes resident after its last tick.\n\
2028 # TYPE superfluid_fim_satellite_lanes_active gauge\n\
2029 superfluid_fim_satellite_lanes_active {}\n\
2030 # HELP superfluid_fim_satellite_pool_blocks_used Satellite KV paged blocks in use.\n\
2031 # TYPE superfluid_fim_satellite_pool_blocks_used gauge\n\
2032 superfluid_fim_satellite_pool_blocks_used {}\n\
2033 # HELP superfluid_fim_satellite_pool_blocks_total Satellite KV paged blocks total.\n\
2034 # TYPE superfluid_fim_satellite_pool_blocks_total gauge\n\
2035 superfluid_fim_satellite_pool_blocks_total {}\n",
2036 s.decode_tokens.load(Relaxed),
2037 s.lanes_active.load(Relaxed),
2038 s.pool_blocks_used.load(Relaxed),
2039 s.pool_blocks_total.load(Relaxed),
2040 ));
2041 }
2042 out
2043 }
2044
2045 pub fn set_background_tick_divisor(&self, divisor: u32) {
2046 self.background_tick_divisor
2047 .store(divisor.max(1), std::sync::atomic::Ordering::Relaxed);
2048 }
2049
2050 pub fn set_qos(
2051 &self,
2052 session: u64,
2053 class: u8,
2054 batch_invariant: bool,
2055 ) -> Result<CommittedEvent, DaemonError> {
2056 if batch_invariant && self.fleet.is_some() {
2057 return Err(fleet_daemon::batch_invariant_refused());
2058 }
2059 self.store
2060 .lock()
2061 .expect("store")
2062 .set_qos(session, class, batch_invariant)
2063 }
2064
2065 pub fn sched_stats(&self) -> std::sync::Arc<scheduler::SchedStats> {
2066 std::sync::Arc::clone(&self.stats)
2067 }
2068
2069 pub fn cancel_registry(&self) -> std::sync::Arc<CancelSet> {
2070 std::sync::Arc::clone(&self.cancels)
2071 }
2072
2073 pub fn active_registry(
2074 &self,
2075 ) -> std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>> {
2076 std::sync::Arc::clone(&self.active)
2077 }
2078
2079 pub fn store(&self) -> std::sync::Arc<std::sync::Mutex<SessionStore>> {
2080 std::sync::Arc::clone(&self.store)
2081 }
2082
2083 pub fn bus(&self) -> std::sync::Arc<bus::SessionBus> {
2084 self.store.lock().expect("store").bus()
2085 }
2086
2087 pub fn shutdown(&self) {
2088 self.serving().sched.shutdown();
2089 }
2090
2091 pub fn signal_shutdown(&self) {
2092 self.serving().sched.signal_shutdown();
2093 }
2094
2095 pub fn create(&self, parent: Option<u64>, params: GenParams) -> Result<u64, DaemonError> {
2096 self.store.lock().expect("store").create(parent, params)
2097 }
2098
2099 pub fn fork(
2100 &self,
2101 parent: u64,
2102 at_event: u64,
2103 params: Option<GenParams>,
2104 ) -> Result<u64, DaemonError> {
2105 self.refuse_if_purging(parent)?;
2106 self.store.lock().expect("store").fork(parent, at_event, params)
2107 }
2108
2109 pub fn tree(&self) -> Vec<(u64, Option<u64>, u64)> {
2110 self.sessions(false)
2111 .into_iter()
2112 .map(|s| (s.id, s.parent, s.fork_at))
2113 .collect()
2114 }
2115
2116 pub fn sessions(&self, include_archived: bool) -> Vec<SessionSummary> {
2117 let store = self.store.lock().expect("store");
2118 store
2119 .session_ids()
2120 .into_iter()
2121 .filter_map(|id| store.session(id).ok())
2122 .filter(|s| include_archived || !s.archived)
2123 .map(|s| SessionSummary {
2124 id: s.id,
2125 parent: if s.rerooted { None } else { s.parent },
2126 fork_at: s.base,
2127 title: s.title.clone(),
2128 archived: s.archived,
2129 meta_version: s.meta_version,
2130 generation: s.generation,
2131 events: s.events.len() as u64,
2132 tokens: s.tokens.len() as u64,
2133 open_tool_calls: s.open_tool_calls.len() as u64,
2134 })
2135 .collect()
2136 }
2137
2138 pub fn set_meta(
2139 &self,
2140 session: u64,
2141 expected_version: u64,
2142 title: Option<String>,
2143 archived: Option<bool>,
2144 ) -> Result<u64, DaemonError> {
2145 self.store
2146 .lock()
2147 .expect("store")
2148 .set_meta(session, expected_version, title, archived)
2149 }
2150
2151 pub fn rebase(
2152 &self,
2153 parent: u64,
2154 edits: Vec<wal::RebaseEdit>,
2155 params: Option<GenParams>,
2156 ) -> Result<u64, DaemonError> {
2157 use wal::RebaseEdit;
2158 if edits.is_empty() {
2159 return Err(DaemonError::RebaseSpec("no edits"));
2160 }
2161 self.refuse_if_purging(parent)?;
2162 let range = |e: &RebaseEdit| match e {
2163 RebaseEdit::Drop { from, to } | RebaseEdit::Replace { from, to, .. } => (*from, *to),
2164 };
2165 let mut store = self.store.lock().expect("store");
2166 let (max, parent_events) = {
2167 let p = store.session(parent)?;
2168 (p.next_event_id(), p.events.clone())
2169 };
2170 let mut last_to = 0;
2171 for e in &edits {
2172 let (from, to) = range(e);
2173 if from == 0 {
2174 return Err(DaemonError::RebaseSpec("edits never include Created (id 0)"));
2175 }
2176 if from >= to {
2177 return Err(DaemonError::RebaseSpec("empty range"));
2178 }
2179 if to > max {
2180 return Err(DaemonError::RebaseSpec("range beyond the parent's committed events"));
2181 }
2182 if from < last_to {
2183 return Err(DaemonError::RebaseSpec("ranges must be sorted and non-overlapping"));
2184 }
2185 if let RebaseEdit::Replace { role, .. } = e {
2186 if !matches!(*role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2187 return Err(DaemonError::UnknownRole(*role));
2188 }
2189 }
2190 last_to = to;
2191 }
2192 let first = range(&edits[0]).0;
2193 let child = store.rebase_branch(parent, first, params, edits.clone())?;
2194 let mut tool_ids: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
2195 let mut i = first as usize;
2196 while i < parent_events.len() {
2197 let id = i as u64;
2198 if let Some(e) = edits.iter().find(|e| {
2199 let (from, to) = range(e);
2200 id >= from && id < to
2201 }) {
2202 let (from, to) = range(e);
2203 if let RebaseEdit::Replace { role, text, .. } = e {
2204 if id == from {
2205 let span = self.render_for(&store, child, *role, text)?;
2206 store.append_message(child, *role, text.clone(), span)?;
2207 }
2208 }
2209 i = to as usize;
2210 continue;
2211 }
2212 match &parent_events[i].body {
2213 EventBody::Appended { text, span } => {
2214 store.append(child, text.clone(), span.clone())?;
2215 }
2216 EventBody::Message { role, text, span } => {
2217 store.append_message(child, *role, text.clone(), span.clone())?;
2218 }
2219 EventBody::GenerationPrompt { span } => {
2220 store.commit_generation_prompt(child, span.clone())?;
2221 }
2222 EventBody::Generated {
2223 span,
2224 text,
2225 channel,
2226 finish,
2227 } => {
2228 store.commit_generated(child, span.clone(), text.clone(), *channel, *finish)?;
2229 }
2230 EventBody::ToolUse { name, arguments } => {
2231 let e = store.commit_tool_use(child, name.clone(), arguments.clone())?;
2232 tool_ids.insert(id, e.event_id);
2233 }
2234 EventBody::ToolResult {
2235 call_id,
2236 content,
2237 span,
2238 } => match tool_ids.get(call_id) {
2239 Some(new_id) => {
2240 store.commit_tool_result(child, *new_id, content.clone(), span.clone())?;
2241 }
2242 None => {
2243 store.append_message(child, wal::role::TOOL, content.clone(), span.clone())?;
2244 }
2245 },
2246 EventBody::ToolParseFailure { raw } => {
2247 store.commit_tool_parse_failure(child, raw.clone())?;
2248 }
2249 EventBody::Created { .. }
2250 | EventBody::EpochBump
2251 | EventBody::GenerationFingerprint { .. }
2252 | EventBody::Forked { .. }
2253 | EventBody::Rebased { .. }
2254 | EventBody::MetaUpdated { .. }
2255 | EventBody::ToolExpired { .. }
2256 | EventBody::Purged { .. }
2257 | EventBody::Rerooted { .. }
2258 | EventBody::QosSet { .. } => {}
2259 EventBody::Block {
2260 role,
2261 kind,
2262 payload,
2263 span,
2264 visibility_version,
2265 } => {
2266 store.commit_block(child, *role, *kind, payload.clone(), span.clone(), *visibility_version)?;
2267 }
2268 EventBody::PermissionRequest { .. }
2269 | EventBody::PermissionResponse { .. }
2270 | EventBody::ToolCancelRequested { .. }
2271 | EventBody::ToolOutcome { .. }
2272 | EventBody::ToolReconciliation { .. }
2273 | EventBody::ToolLease { .. }
2274 | EventBody::Pinned { .. } => {}
2275 }
2276 i += 1;
2277 }
2278 Ok(child)
2279 }
2280
2281 pub fn trim(&self, session: u64, to_event: u64) -> Result<u64, DaemonError> {
2282 let max = {
2283 let store = self.store.lock().expect("store");
2284 store.session(session)?.next_event_id()
2285 };
2286 if to_event == 0 || to_event > max {
2287 return Err(DaemonError::ForkPoint {
2288 session,
2289 fork_at: to_event,
2290 max,
2291 });
2292 }
2293 if to_event == max {
2294 return self.fork(session, to_event, None);
2295 }
2296 self.rebase(
2297 session,
2298 vec![wal::RebaseEdit::Drop {
2299 from: to_event,
2300 to: max,
2301 }],
2302 None,
2303 )
2304 }
2305
2306 pub fn pin(&self, session: u64, ttl_ms: u64) -> Result<u64, DaemonError> {
2307 let deadline = if ttl_ms == 0 {
2308 0
2309 } else {
2310 wal::now_unix_ms().saturating_add(ttl_ms)
2311 };
2312 let (tokens, want) = {
2313 let mut store = self.store.lock().expect("store");
2314 store.set_pin(session, deadline)?;
2315 pins::session_target(&store.session(session)?.tokens)
2316 };
2317 self.send_pin(
2318 pins::PinKey::Session(session),
2319 tokens,
2320 want,
2321 deadline,
2322 Some(session),
2323 )?;
2324 Ok(deadline)
2325 }
2326
2327 pub fn pin_prefix(
2328 &self,
2329 anchor: Option<u64>,
2330 stream: &[u32],
2331 upto: usize,
2332 ttl_ms: u64,
2333 ) -> Result<u64, DaemonError> {
2334 let upto = upto.min(stream.len());
2335 let key = pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]));
2336 let tokens = if upto < stream.len() {
2337 stream[..=upto].to_vec()
2338 } else {
2339 pins::session_target(stream).0
2340 };
2341 let deadline = wal::now_unix_ms().saturating_add(ttl_ms.max(1));
2342 self.send_pin(key, tokens, upto as u64, deadline, anchor)
2343 }
2344
2345 pub fn pinned_prefix_tokens(
2346 &self,
2347 stream: &[u32],
2348 uptos: &[usize],
2349 ) -> Result<u64, DaemonError> {
2350 let keys = uptos
2351 .iter()
2352 .map(|&u| {
2353 let upto = u.min(stream.len());
2354 pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]))
2355 })
2356 .collect();
2357 let (tx, rx) = std::sync::mpsc::channel();
2358 self.sched_tx()
2359 .send(scheduler::SchedCmd::PinHeld(keys, tx))
2360 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2361 rx.recv()
2362 .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))
2363 }
2364
2365 pub fn unanchor_pins(&self, session: u64) {
2366 let _ = self.sched_tx().send(scheduler::SchedCmd::PinUnanchor(session));
2367 }
2368
2369 fn send_pin(
2370 &self,
2371 key: pins::PinKey,
2372 tokens: Vec<u32>,
2373 want: u64,
2374 deadline_unix_ms: u64,
2375 anchor: Option<u64>,
2376 ) -> Result<u64, DaemonError> {
2377 let (tx, rx) = std::sync::mpsc::channel();
2378 self.sched_tx()
2379 .send(scheduler::SchedCmd::Pin(pins::PinReq {
2380 key,
2381 tokens,
2382 want,
2383 deadline_unix_ms,
2384 anchor,
2385 reply: Some(tx),
2386 }))
2387 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2388 rx.recv()
2389 .map_err(|_| DaemonError::Protocol("scheduler dropped the pin"))
2390 }
2391
2392 fn render_for(
2393 &self,
2394 store: &SessionStore,
2395 session: u64,
2396 role: u32,
2397 text: &str,
2398 ) -> Result<Vec<u32>, DaemonError> {
2399 let s = store.session(session)?;
2400 let mut span = if s.tokens.is_empty() {
2401 self.codec().stream_prologue()
2402 } else {
2403 match Self::last_stream_record(&s.events) {
2404 Some(EventBody::Generated { finish, .. })
2405 if *finish == superfluid_abi::finish::EOS =>
2406 {
2407 self.codec().turn_separator().unwrap_or_default()
2408 }
2409 _ => Vec::new(),
2410 }
2411 };
2412 match self.codec().render_message(role, text) {
2413 Some(rendered) => span.extend(rendered),
2414 None => {
2415 let encoded = self.codec().encode(text);
2416 if encoded.is_empty() {
2417 return Err(DaemonError::Protocol("codec produced no span for the replacement"));
2418 }
2419 span.extend(encoded);
2420 }
2421 }
2422 Ok(span)
2423 }
2424
2425 pub fn purge(
2426 &self,
2427 session: u64,
2428 expected_generation: u64,
2429 mode: wal::PurgeMode,
2430 ) -> Result<Vec<u64>, DaemonError> {
2431 let targets: Vec<u64> = {
2432 let store = self.store.lock().expect("store");
2433 let s = store.session(session)?;
2434 if s.generation != expected_generation {
2435 return Err(DaemonError::GenerationConflict {
2436 session,
2437 expected: expected_generation,
2438 actual: s.generation,
2439 });
2440 }
2441 let mut t = vec![session];
2442 if mode == wal::PurgeMode::Cascade {
2443 t.extend(store.descendants_of(session));
2444 }
2445 t
2446 };
2447 {
2448 let _a = self.active.lock().expect("active set");
2449 let mut p = self.purging.lock().expect("purging");
2450 p.extend(targets.iter().copied());
2451 }
2452 let _fence = PurgeFence {
2453 set: std::sync::Arc::clone(&self.purging),
2454 ids: targets.clone(),
2455 };
2456 for &t in &targets {
2457 self.quiesce(t)?;
2458 }
2459 let purged = {
2460 let mut store = self.store.lock().expect("store");
2461 for &t in &targets {
2462 let open: Vec<u64> = store.session(t)?.open_tool_calls.keys().copied().collect();
2463 for call in open {
2464 store.expire_tool_call(t, call, "purged")?;
2465 }
2466 }
2467 store.purge(session, expected_generation, mode)?
2468 };
2469 let bus = self.bus();
2470 for &p in &purged {
2471 bus.end_session(p, bus::EndReason::Purged);
2472 }
2473 for &p in &purged {
2474 let _ = self.send_pin(pins::PinKey::Session(p), Vec::new(), 0, 0, None);
2475 }
2476 Ok(purged)
2477 }
2478
2479 fn refuse_if_purging(&self, session: u64) -> Result<(), DaemonError> {
2480 let _a = self.active.lock().expect("active set");
2481 if self.purging.lock().expect("purging").contains(&session) {
2482 return Err(DaemonError::Purged(session));
2483 }
2484 Ok(())
2485 }
2486
2487 fn quiesce(&self, session: u64) -> Result<(), DaemonError> {
2488 {
2489 let mut c = self.cancels.lock().expect("cancel registry");
2490 if self.active.lock().expect("active set").contains(&session) {
2491 c.insert(session);
2492 } else {
2493 return Ok(());
2494 }
2495 }
2496 self.cancels.wake();
2497 for _ in 0..2000 {
2498 if !self.active.lock().expect("active set").contains(&session) {
2499 return Ok(());
2500 }
2501 std::thread::sleep(std::time::Duration::from_millis(5));
2502 }
2503 Err(DaemonError::PurgeTimeout(session))
2504 }
2505
2506 pub fn append(
2507 &self,
2508 session: u64,
2509 text: Option<String>,
2510 span: Vec<u32>,
2511 ) -> Result<CommittedEvent, DaemonError> {
2512 {
2513 let active = self.active.lock().expect("active set");
2514 if active.contains(&session) {
2515 return Err(DaemonError::SessionBusy(session));
2516 }
2517 if self.purging.lock().expect("purging").contains(&session) {
2518 return Err(DaemonError::Purged(session));
2519 }
2520 }
2521 let span = if span.is_empty() {
2522 match &text {
2523 Some(t) => {
2524 let encoded = self.codec().encode(t);
2525 if encoded.is_empty() {
2526 return Err(DaemonError::Protocol(
2527 "append had no span and the codec produced none",
2528 ));
2529 }
2530 encoded
2531 }
2532 None => return Err(DaemonError::Protocol("append carried neither text nor span")),
2533 }
2534 } else {
2535 span
2536 };
2537 self.store.lock().expect("store").append(session, text, span)
2538 }
2539
2540 fn separator_for(&self, session: u64) -> Vec<u32> {
2542 let (empty, ended) = {
2543 let store = self.store.lock().expect("store");
2544 let Ok(s) = store.session(session) else {
2545 return Vec::new();
2546 };
2547 (
2548 s.tokens.is_empty(),
2549 matches!(
2550 Self::last_stream_record(&s.events),
2551 Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS
2552 ),
2553 )
2554 };
2555 if empty {
2556 self.codec().stream_prologue()
2557 } else if ended {
2558 self.codec().turn_separator().unwrap_or_default()
2559 } else {
2560 Vec::new()
2561 }
2562 }
2563
2564 pub fn append_message(
2565 &self,
2566 session: u64,
2567 role: u32,
2568 text: String,
2569 ) -> Result<CommittedEvent, DaemonError> {
2570 if self.active.lock().expect("active set").contains(&session) {
2571 return Err(DaemonError::SessionBusy(session));
2572 }
2573 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2574 return Err(DaemonError::UnknownRole(role));
2575 }
2576 let mut span = self.separator_for(session);
2577 span.extend(
2578 self.codec()
2579 .render_message(role, &text)
2580 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2581 );
2582 self.store
2583 .lock()
2584 .expect("store")
2585 .append_message(session, role, text, span)
2586 }
2587
2588 pub fn renders_per_message(&self) -> bool {
2589 self.codec().renders_per_message()
2590 }
2591
2592 pub fn append_system_full(
2593 &self,
2594 session: u64,
2595 system: Option<String>,
2596 tool_jsons: Vec<String>,
2597 kwargs: &serde_json::Map<String, serde_json::Value>,
2598 ) -> Result<CommittedEvent, DaemonError> {
2599 if self.active.lock().expect("active set").contains(&session) {
2600 return Err(DaemonError::SessionBusy(session));
2601 }
2602 let span = self
2603 .codec()
2604 .render_system_full(system.as_deref(), &tool_jsons, kwargs)
2605 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2606 self.store.lock().expect("store").append_message(
2607 session,
2608 wal::role::SYSTEM,
2609 system.unwrap_or_default(),
2610 span,
2611 )
2612 }
2613
2614 pub fn append_system(
2617 &self,
2618 session: u64,
2619 system: Option<String>,
2620 tools: Vec<String>,
2621 ) -> Result<CommittedEvent, DaemonError> {
2622 if system.is_none() && tools.is_empty() {
2623 return Err(DaemonError::Protocol("AppendSystem carried neither text nor tools"));
2624 }
2625 let tool_jsons = tools
2626 .iter()
2627 .enumerate()
2628 .map(|(index, t)| {
2629 codec::function_tool(t).map_err(|why| DaemonError::InvalidTool { index, why })
2630 })
2631 .collect::<Result<Vec<_>, _>>()?;
2632 if self.active.lock().expect("active set").contains(&session) {
2633 return Err(DaemonError::SessionBusy(session));
2634 }
2635 let codec = self.codec();
2636 let mut span = codec.stream_prologue();
2637 span.extend(
2638 codec
2639 .render_system_full(system.as_deref(), &tool_jsons, &serde_json::Map::new())
2640 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2641 );
2642 let mut store = self.store.lock().expect("store");
2643 if !store.session(session)?.tokens.is_empty() {
2644 return Err(DaemonError::SystemNotFirst(session));
2645 }
2646 store.append_message(session, wal::role::SYSTEM, system.unwrap_or_default(), span)
2647 }
2648
2649 pub(crate) fn render_conversation(
2650 &self,
2651 messages: &[codec::ChatMessage],
2652 tools: &[String],
2653 ) -> Option<Vec<u32>> {
2654 self.codec()
2655 .render_prompt_structured_with(messages, tools, &serde_json::Map::new())
2656 }
2657
2658 pub fn supports_reasoning_effort(&self) -> bool {
2659 self.codec().supports_reasoning_effort()
2660 }
2661
2662 pub fn reasoning_effort_levels(&self) -> Vec<&'static str> {
2663 self.codec().reasoning_effort_levels()
2664 }
2665
2666 pub fn supports_enable_thinking(&self) -> bool {
2667 self.codec().supports_enable_thinking()
2668 }
2669
2670 pub fn validate_template_kwargs(
2671 &self,
2672 kwargs: &serde_json::Map<String, serde_json::Value>,
2673 ) -> Result<(), String> {
2674 self.codec().validate_template_kwargs(kwargs)
2675 }
2676
2677 fn commit_opener(&self, session: u64, opener: Vec<u32>) -> Result<(), DaemonError> {
2678 if opener.is_empty() {
2679 return Ok(());
2680 }
2681 self.store
2682 .lock()
2683 .expect("store")
2684 .commit_generation_prompt(session, opener)
2685 .map(|_| ())
2686 }
2687
2688 pub fn append_conversation(
2689 &self,
2690 session: u64,
2691 messages: &[crate::codec::ChatMessage],
2692 tools: &[String],
2693 kwargs: &serde_json::Map<String, serde_json::Value>,
2694 ) -> Result<CommittedEvent, DaemonError> {
2695 let codec = self.codec();
2696 let mut span = codec
2697 .render_prompt_structured_with(messages, tools, kwargs)
2698 .ok_or_else(|| render_refusal(&*codec, messages, tools, kwargs, "codec has no chat dialect"))?;
2699 let opener_len = codec.trailing_generation_prompt(messages, tools, kwargs, &span);
2700 let opener: Vec<u32> = if opener_len > 0 && opener_len < span.len() {
2701 span.split_off(span.len() - opener_len)
2702 } else {
2703 Vec::new()
2704 };
2705 let text = messages.last().map(|m| m.content.clone());
2706 let blobs: Vec<(u32, String)> = messages
2707 .iter()
2708 .flat_map(|m| {
2709 m.parts.iter().filter_map(move |p| match p {
2710 crate::codec::ContentPart::Image { blob } => Some((m.role, blob.clone())),
2711 crate::codec::ContentPart::Audio { blob } => Some((m.role, blob.clone())),
2712 crate::codec::ContentPart::Text(_) => None,
2713 })
2714 })
2715 .collect();
2716 if blobs.is_empty() {
2717 let ev = self.append(session, text, span)?;
2718 self.commit_opener(session, opener)?;
2719 return Ok(ev);
2720 }
2721 let mut probes = Vec::with_capacity(blobs.len());
2722 for (_, blob) in &blobs {
2723 if !self.media.exists(blob) {
2724 return Err(DaemonError::UnknownMedia(blob.clone()));
2725 }
2726 let path = self.media.path_of(blob);
2727 let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
2728 image_path: path.to_string_lossy().into_owned(),
2729 })? {
2730 proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
2731 _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
2732 };
2733 if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
2734 return Err(DaemonError::MediaTooLarge(info.n_tokens));
2735 }
2736 probes.push(info);
2737 }
2738 let mut out = Vec::with_capacity(span.len() + 256);
2739 let mut offsets: Vec<usize> = Vec::with_capacity(probes.len());
2740 let mut img = 0usize;
2741 for (idx, &t) in span.iter().enumerate() {
2742 if img == probes.len() || t != probes[img].image_token_id {
2743 if probes.iter().any(|p| p.image_token_id == t) {
2744 return Err(DaemonError::Protocol(
2745 "rendered prompt contains a media marker with no matching pending part",
2746 ));
2747 }
2748 out.push(t);
2749 continue;
2750 }
2751 let info = &probes[img];
2752 if info.boi_token_id != 0 && out.last() != Some(&info.boi_token_id) {
2753 out.push(info.boi_token_id);
2754 }
2755 offsets.push(out.len());
2756 out.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
2757 if info.eoi_token_id != 0 && span.get(idx + 1) != Some(&info.eoi_token_id) {
2758 out.push(info.eoi_token_id);
2759 }
2760 img += 1;
2761 }
2762 if img != probes.len() {
2763 return Err(DaemonError::Protocol(
2764 "this model's chat template did not emit an image marker for every image part; it cannot place media",
2765 ));
2766 }
2767 let stream_len = {
2768 let store = self.store.lock().expect("store");
2769 store.session(session)?.tokens.len() as u64
2770 };
2771 let ev = self.append(session, text, out)?;
2772 for (((role, blob), info), off) in blobs.iter().zip(&probes).zip(&offsets) {
2773 let payload = serde_json::json!({
2774 "blob": blob,
2775 "n_tokens": info.n_tokens,
2776 "image_token_id": info.image_token_id,
2777 "offset": stream_len + *off as u64,
2778 "pre_text": "",
2779 "post_text": "",
2780 "media_identity": {
2781 "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
2782 "encoder": "bundle-weights-identity",
2783 },
2784 })
2785 .to_string();
2786 let mut store = self.store.lock().expect("store");
2787 store.commit_block(
2788 session,
2789 *role,
2790 wal::block_kind::IMAGE,
2791 payload,
2792 Vec::new(),
2793 codec::MODEL_VISIBILITY_VERSION,
2794 )?;
2795 }
2796 self.commit_opener(session, opener)?;
2797 Ok(ev)
2798 }
2799
2800 pub fn response_format_tag(&self, json_schema: &str) -> Option<String> {
2801 self.codec().response_format_tag(json_schema)
2802 }
2803
2804 pub fn tool_structural_tag(&self, tool_jsons: &[String], at_least_one: bool) -> Option<String> {
2805 self.codec().structural_tag(tool_jsons, at_least_one)
2806 }
2807
2808 pub fn grammar_create_structural(&self, tag_json: &str) -> Result<u32, DaemonError> {
2809 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammarStructural {
2810 tag_json: tag_json.to_string(),
2811 })? {
2812 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
2813 _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
2814 }
2815 }
2816
2817 pub fn parse_tool_call(&self, raw: &str) -> Option<(String, String)> {
2818 self.codec().parse_tool_call(raw)
2819 }
2820
2821 pub fn named_tool_call(&self, raw: &str) -> Option<(String, String)> {
2822 self.codec().named_tool_call(raw)
2823 }
2824
2825 pub fn parse_tool_call_with(
2826 &self,
2827 raw: &str,
2828 schemas: Option<&crate::codec::ToolSchemas>,
2829 ) -> Option<(String, String)> {
2830 self.codec().parse_tool_call_with(raw, schemas)
2831 }
2832
2833 pub fn tool_fragment_mode(&self) -> crate::tool_fragment::ToolFragmentMode {
2834 self.codec().tool_fragment_mode()
2835 }
2836
2837 pub fn tool_call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
2838 self.codec().call_grammar(tool_jsons)
2839 }
2840
2841 pub fn append_system_with_tools(
2842 &self,
2843 session: u64,
2844 system: Option<String>,
2845 tool_jsons: Vec<String>,
2846 ) -> Result<CommittedEvent, DaemonError> {
2847 if self.active.lock().expect("active set").contains(&session) {
2848 return Err(DaemonError::SessionBusy(session));
2849 }
2850 let span = self
2851 .codec()
2852 .render_system_with_tools(system.as_deref(), &tool_jsons)
2853 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?;
2854 self.store.lock().expect("store").append_message(
2855 session,
2856 wal::role::SYSTEM,
2857 system.unwrap_or_default(),
2858 span,
2859 )
2860 }
2861
2862 pub fn append_assistant_with_tool_calls(
2865 &self,
2866 session: u64,
2867 content: String,
2868 calls: Vec<(String, String)>,
2869 after_query: bool,
2870 ) -> Result<CommittedEvent, DaemonError> {
2871 if self.active.lock().expect("active set").contains(&session) {
2872 return Err(DaemonError::SessionBusy(session));
2873 }
2874 let mut span = self.separator_for(session);
2875 let codec = self.codec();
2876 let turn = if after_query {
2877 codec.render_assistant_with_tool_calls_after_query(&content, &calls)
2878 } else {
2879 codec.render_assistant_with_tool_calls(&content, &calls)
2880 };
2881 span.extend(turn.ok_or(DaemonError::Protocol("codec has no tool dialect"))?);
2882 self.store.lock().expect("store").append_message(
2883 session,
2884 wal::role::ASSISTANT,
2885 content,
2886 span,
2887 )
2888 }
2889
2890 pub fn append_tool_result(
2891 &self,
2892 session: u64,
2893 call_id: u64,
2894 content: String,
2895 ) -> Result<CommittedEvent, DaemonError> {
2896 if self.active.lock().expect("active set").contains(&session) {
2897 return Err(DaemonError::SessionBusy(session));
2898 }
2899 let name = {
2900 let mut store = self.store.lock().expect("store");
2901 let s = store.session(session)?;
2902 if call_id < s.base {
2903 return Err(DaemonError::InheritedToolCall(call_id));
2904 }
2905 match s.ledger.get(&call_id) {
2906 None => return Err(DaemonError::UnknownToolCall(call_id)),
2907 Some(e)
2908 if e.state == wal::ledger_state::OPEN
2909 || e.state == wal::ledger_state::CANCEL_REQUESTED =>
2910 {
2911 if let Some(deadline) = e.lease_deadline_unix_ms {
2912 if wal::now_unix_ms() > deadline {
2913 store.expire_tool_call(session, call_id, "lease expired")?;
2914 return Err(DaemonError::ToolLeaseExpired(call_id));
2915 }
2916 }
2917 e.name.clone()
2918 }
2919 Some(e) if e.state == wal::ledger_state::SUCCEEDED => {
2920 return Err(DaemonError::UnknownToolCall(call_id));
2921 }
2922 Some(_) => {
2923 store.commit_tool_reconciliation(
2924 session,
2925 call_id,
2926 format!("late result: {} bytes", content.len()),
2927 )?;
2928 return Err(DaemonError::ToolLateEffect(call_id));
2929 }
2930 }
2931 };
2932 let mut span = self.separator_for(session);
2933 span.extend(
2934 self.codec()
2935 .render_tool_result(&name, &content)
2936 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
2937 );
2938 self.store
2939 .lock()
2940 .expect("store")
2941 .commit_tool_result(session, call_id, content, span)
2942 }
2943
2944 pub fn append_block(
2945 &self,
2946 session: u64,
2947 role: u32,
2948 kind: u32,
2949 payload: String,
2950 ) -> Result<CommittedEvent, DaemonError> {
2951 if self.active.lock().expect("active set").contains(&session) {
2952 return Err(DaemonError::SessionBusy(session));
2953 }
2954 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT | wal::role::TOOL) {
2955 return Err(DaemonError::UnknownRole(role));
2956 }
2957 if codec::project_block_text(kind, &payload).is_none() {
2958 return Err(DaemonError::UnknownBlockKind(kind));
2959 }
2960 let projected = self
2961 .codec()
2962 .render_block(role, kind, &payload)
2963 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2964 let span = if projected.is_empty() {
2965 Vec::new()
2966 } else {
2967 let mut span = self.separator_for(session);
2968 span.extend(projected);
2969 span
2970 };
2971 self.store.lock().expect("store").commit_block(
2972 session,
2973 role,
2974 kind,
2975 payload,
2976 span,
2977 codec::MODEL_VISIBILITY_VERSION,
2978 )
2979 }
2980
2981 pub fn request_permission(
2982 &self,
2983 session: u64,
2984 call_id: u64,
2985 text: String,
2986 ) -> Result<CommittedEvent, DaemonError> {
2987 let active = self.active.lock().expect("active set");
2988 if active.contains(&session) {
2989 return Err(DaemonError::SessionBusy(session));
2990 }
2991 if self.purging.lock().expect("purging").contains(&session) {
2992 return Err(DaemonError::Purged(session));
2993 }
2994 self.store
2995 .lock()
2996 .expect("store")
2997 .commit_permission_request(session, call_id, text)
2998 }
2999
3000 pub fn respond_permission(
3001 &self,
3002 session: u64,
3003 request_id: u64,
3004 granted: bool,
3005 ) -> Result<CommittedEvent, DaemonError> {
3006 self.store
3007 .lock()
3008 .expect("store")
3009 .commit_permission_response(session, request_id, granted)
3010 }
3011
3012 pub fn request_tool_cancel(&self, session: u64, call_id: u64) -> Result<CommittedEvent, DaemonError> {
3013 self.store
3014 .lock()
3015 .expect("store")
3016 .commit_tool_cancel_request(session, call_id)
3017 }
3018
3019 pub fn append_tool_outcome(
3020 &self,
3021 session: u64,
3022 call_id: u64,
3023 outcome: u8,
3024 note: String,
3025 ) -> Result<CommittedEvent, DaemonError> {
3026 self.store
3027 .lock()
3028 .expect("store")
3029 .commit_tool_outcome(session, call_id, outcome, note)
3030 }
3031
3032 pub fn ledger(&self, session: u64) -> Result<Vec<(u64, store::LedgerEntry)>, DaemonError> {
3033 Ok(self
3034 .store
3035 .lock()
3036 .expect("store")
3037 .session(session)?
3038 .ledger
3039 .iter()
3040 .map(|(id, e)| (*id, e.clone()))
3041 .collect())
3042 }
3043
3044 pub fn append_tool_history(
3045 &self,
3046 session: u64,
3047 content: String,
3048 ) -> Result<CommittedEvent, DaemonError> {
3049 if self.active.lock().expect("active set").contains(&session) {
3050 return Err(DaemonError::SessionBusy(session));
3051 }
3052 let mut span = self.separator_for(session);
3053 span.extend(
3054 self.codec()
3055 .render_tool_result("", &content)
3056 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
3057 );
3058 self.store.lock().expect("store").append_message(
3059 session,
3060 wal::role::TOOL,
3061 content,
3062 span,
3063 )
3064 }
3065
3066 pub fn open_tool_calls(
3067 &self,
3068 session: u64,
3069 ) -> Result<Vec<(u64, String, String)>, DaemonError> {
3070 Ok(self
3071 .store
3072 .lock()
3073 .expect("store")
3074 .session(session)?
3075 .open_tool_calls
3076 .iter()
3077 .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
3078 .collect())
3079 }
3080
3081 pub fn generate(&self, session: u64, max_tokens: u32) -> Result<GenerateOutcome, DaemonError> {
3082 self.generate_streaming(session, max_tokens, |_| Ok(()))
3083 }
3084
3085 pub fn generate_streaming(
3086 &self,
3087 session: u64,
3088 max_tokens: u32,
3089 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3090 ) -> Result<GenerateOutcome, DaemonError> {
3091 self.generate_streaming_ex(session, max_tokens, scheduler::GenExtras::default(), on_event)
3092 }
3093
3094 pub fn generate_streaming_ex(
3095 &self,
3096 session: u64,
3097 max_tokens: u32,
3098 extras: scheduler::GenExtras,
3099 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3100 ) -> Result<GenerateOutcome, DaemonError> {
3101 self.generate_streaming_ex_lp(session, max_tokens, extras, on_event, |_| {})
3102 }
3103
3104 pub fn generate_streaming_ex_lp(
3105 &self,
3106 session: u64,
3107 max_tokens: u32,
3108 extras: scheduler::GenExtras,
3109 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3110 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
3111 ) -> Result<GenerateOutcome, DaemonError> {
3112 self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, |_, _, _| {})
3113 }
3114
3115 pub fn generate_streaming_delta(
3116 &self,
3117 session: u64,
3118 max_tokens: u32,
3119 mut extras: scheduler::GenExtras,
3120 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3121 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
3122 on_delta: impl FnMut(u32, &str, u32),
3123 ) -> Result<GenerateOutcome, DaemonError> {
3124 extras.wants_deltas = true;
3125 self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, on_delta)
3126 }
3127
3128 fn opener_turn(events: &[store::CommittedEvent]) -> Option<codec::TurnState> {
3132 match Self::last_stream_record(events) {
3133 Some(EventBody::Message { .. }) | Some(EventBody::ToolResult { .. }) => Some(codec::TurnState::AfterInput),
3134 Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS => {
3135 Some(codec::TurnState::AfterFinishedTurn)
3136 }
3137 _ => None,
3138 }
3139 }
3140
3141 fn generation_opener(&self, turn: Option<codec::TurnState>, thinking: Option<bool>) -> Option<Vec<u32>> {
3142 turn.and_then(|t| self.codec().generation_prefix_with(t, thinking))
3143 }
3144
3145 pub(crate) fn session_end(&self, session: u64) -> Option<(u64, bool)> {
3149 let store = self.store.lock().expect("store");
3150 let s = store.session(session).ok()?;
3151 let ended = matches!(
3152 Self::last_stream_record(&s.events),
3153 Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS
3154 );
3155 Some((s.next_event_id(), ended))
3156 }
3157
3158 fn last_stream_record(events: &[store::CommittedEvent]) -> Option<&EventBody> {
3159 events
3160 .iter()
3161 .rev()
3162 .map(|e| &e.body)
3163 .find(|b| b.span().is_some_and(|s| !s.is_empty()))
3164 }
3165
3166 fn open_turn_tokens(events: &[store::CommittedEvent]) -> Vec<u32> {
3167 let mut spans: Vec<&[u32]> = Vec::new();
3168 for e in events.iter().rev() {
3169 match &e.body {
3170 EventBody::Generated { span, finish, .. } => {
3171 if spans.is_empty() && *finish == superfluid_abi::finish::EOS {
3172 return Vec::new();
3173 }
3174 spans.push(span);
3175 }
3176 EventBody::GenerationPrompt { span } => {
3177 spans.push(span);
3178 break;
3179 }
3180 b => match b.span() {
3181 None | Some([]) => continue,
3182 Some(_) => break,
3183 },
3184 }
3185 }
3186 spans.into_iter().rev().flatten().copied().collect()
3187 }
3188
3189 fn generate_streaming_inner(
3190 &self,
3191 session: u64,
3192 max_tokens: u32,
3193 extras: scheduler::GenExtras,
3194 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3195 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
3196 on_delta: impl FnMut(u32, &str, u32),
3197 ) -> Result<GenerateOutcome, DaemonError> {
3198 self.ensure_serving()?;
3199 let _span = tracing::info_span!(
3200 parent: None,
3201 "generate",
3202 session,
3203 max_tokens,
3204 model = self.trace_scope.model.as_str(),
3205 store_ns = self.trace_scope.ns
3206 )
3207 .entered();
3208 let (stream, params, class, batch_invariant, media) = {
3209 let mut cancels = self.cancels.lock().expect("cancel registry");
3210 let mut active = self.active.lock().expect("active set");
3211 if self.purging.lock().expect("purging").contains(&session) {
3212 return Err(DaemonError::Purged(session));
3213 }
3214 if self.tool_lease_ms > 0 {
3215 let now = crate::wal::now_unix_ms();
3216 let mut store = self.store.lock().expect("store");
3217 let elapsed: Vec<u64> = {
3218 let s = store.session(session)?;
3219 s.open_tool_calls
3220 .keys()
3221 .filter(|id| {
3222 s.ledger
3223 .get(id)
3224 .and_then(|e| e.lease_deadline_unix_ms)
3225 .is_some_and(|d| d <= now)
3226 })
3227 .copied()
3228 .collect()
3229 };
3230 for call_id in elapsed {
3231 let _ = store.expire_tool_call(session, call_id, "lease elapsed");
3232 }
3233 }
3234 let snap = {
3235 let store = self.store.lock().expect("store");
3236 let s = store.session(session)?;
3237 if !s.open_permissions.is_empty() {
3238 return Err(DaemonError::PermissionPending(session));
3239 }
3240 if !s.open_tool_calls.is_empty() {
3241 tracing::warn!(
3242 session,
3243 open_tool_calls = s.open_tool_calls.len(),
3244 "generating with unanswered tool calls: the model cannot see its own \
3245 tool output and will re-issue the call. Return a result for each open \
3246 call id (OpenAI: a `role: \"tool\"` message carrying its `tool_call_id`), \
3247 or run with --tool-lease-ms so unanswered calls expire instead of \
3248 accumulating."
3249 );
3250 }
3251 (
3252 s.tokens.clone(),
3253 s.params,
3254 s.qos_class,
3255 s.batch_invariant,
3256 self.media_of(s),
3257 )
3258 };
3259 if snap.0.is_empty() {
3260 return Err(DaemonError::EmptySession(session));
3261 }
3262 self.check_stream_fits(snap.0.len())?;
3263 if max_tokens == 0 {
3264 return Ok(GenerateOutcome {
3265 events: Vec::new(),
3266 tokens_generated: 0,
3267 finish: superfluid_abi::finish::NONE,
3268 warm_prefix: 0,
3269 logprobs: Vec::new(),
3270 spec: (0, 0),
3271 decode: (0, 0.0),
3272 });
3273 }
3274 if !active.insert(session) {
3275 return Err(DaemonError::SessionBusy(session));
3276 }
3277 cancels.remove(&session);
3281 snap
3282 };
3283 if let Some(mut fp) = self.codec().behavior_fingerprint() {
3284 fp.speculation = self.serving().speculation.identity_for_fingerprint();
3285 let commit = self
3286 .store
3287 .lock()
3288 .expect("store")
3289 .commit_generation_fingerprint(session, fp.digest());
3290 if let Err(e) = commit {
3291 self.active.lock().expect("active set").remove(&session);
3292 return Err(e);
3293 }
3294 }
3295 let mut stream = stream;
3296 let (turn, open_turn) = {
3297 let store = self.store.lock().expect("store");
3298 let events = &store.session(session)?.events;
3299 (Self::opener_turn(events), Self::open_turn_tokens(events))
3300 };
3301 let opener = self.generation_opener(turn, extras.thinking);
3302 let open_turn = if opener.is_some() { Vec::new() } else { open_turn };
3303 let opener_len = opener.as_ref().map(Vec::len);
3304 let input_end = if opener.is_some() { stream.len() as u64 } else { 0 };
3305 if let Some(op) = opener {
3306 if let Err(e) = self.check_stream_fits(stream.len() + op.len()) {
3307 self.active.lock().expect("active set").remove(&session);
3308 return Err(e);
3309 }
3310 let commit = self
3311 .store
3312 .lock()
3313 .expect("store")
3314 .commit_generation_prompt(session, op.clone());
3315 if let Err(e) = commit {
3316 self.active.lock().expect("active set").remove(&session);
3317 return Err(e);
3318 }
3319 stream.extend_from_slice(&op);
3320 }
3321 let mut extras = extras;
3322 if extras.open_channel == 0 {
3323 let scope: &[u32] = match opener_len {
3324 Some(n) => &stream[stream.len() - n..],
3325 None => &open_turn,
3326 };
3327 let open = codec::trailing_open_channel(scope, &self.codec().channel_markers());
3328 if open != 0 {
3329 extras.open_channel = open;
3330 }
3331 }
3332 let result = scheduler::run_job(
3333 &self.sched_tx(),
3334 session,
3335 stream,
3336 input_end,
3337 params,
3338 max_tokens,
3339 class,
3340 batch_invariant,
3341 media,
3342 extras,
3343 &self.cancels,
3344 on_event,
3345 on_logprobs,
3346 on_delta,
3347 );
3348 match result {
3349 Ok((events, tokens_generated, finish, warm_prefix, logprobs, spec, decode)) => Ok(GenerateOutcome {
3350 events,
3351 tokens_generated,
3352 finish,
3353 warm_prefix,
3354 logprobs,
3355 spec,
3356 decode,
3357 }),
3358 Err(e) => {
3359 self.active.lock().expect("active set").remove(&session);
3360 Err(e)
3361 }
3362 }
3363 }
3364
3365 pub fn read(&self, session: u64, cursor: u64) -> Result<Vec<CommittedEvent>, DaemonError> {
3366 Ok(self.store.lock().expect("store").read(session, cursor)?.to_vec())
3367 }
3368
3369 pub fn session_ids(&self) -> Vec<u64> {
3370 self.store.lock().expect("store").session_ids()
3371 }
3372}
3373
3374#[cfg(test)]
3375mod open_turn_tests {
3376 use super::*;
3377 use superfluid_abi::finish;
3378
3379 fn ev(event_id: u64, body: EventBody) -> store::CommittedEvent {
3380 store::CommittedEvent { event_id, epoch: 0, ts_unix_ms: 0, body }
3381 }
3382
3383 fn generated(span: &[u32], finish: u32) -> EventBody {
3384 EventBody::Generated { span: span.to_vec(), text: String::new(), channel: 0, finish }
3385 }
3386
3387 #[test]
3388 fn open_turn_skips_every_token_less_record() {
3389 let events = vec![
3390 ev(0, EventBody::Message { role: 1, text: "hi".into(), span: vec![900, 901] }),
3391 ev(1, EventBody::GenerationPrompt { span: vec![1, 2] }),
3392 ev(2, generated(&[3, 4], finish::NONE)),
3393 ev(3, EventBody::ToolUse { name: "x".into(), arguments: "{".into() }),
3394 ev(4, EventBody::ToolLease { call_id: 3, deadline_unix_ms: 1 }),
3395 ev(5, EventBody::ToolReconciliation { call_id: 3, note: String::new() }),
3396 ev(6, EventBody::EpochBump),
3397 ev(7, EventBody::Pinned { deadline_unix_ms: 1 }),
3398 ev(8, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3399 ev(9, generated(&[5], finish::LENGTH)),
3400 ];
3401 assert_eq!(Daemon::open_turn_tokens(&events), vec![1, 2, 3, 4, 5]);
3402 }
3403
3404 #[test]
3405 fn token_less_records_do_not_reopen_a_closed_turn() {
3406 let closed = vec![
3407 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3408 ev(1, generated(&[2], finish::EOS)),
3409 ev(2, EventBody::ToolLease { call_id: 9, deadline_unix_ms: 1 }),
3410 ev(3, EventBody::QosSet { class: 0, batch_invariant: false }),
3411 ];
3412 assert!(Daemon::open_turn_tokens(&closed).is_empty());
3413 let after_result = vec![
3414 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3415 ev(1, generated(&[2], finish::NONE)),
3416 ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3417 ev(3, EventBody::ToolResult { call_id: 2, content: "ok".into(), span: vec![7, 8] }),
3418 ev(4, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3419 ];
3420 assert!(Daemon::open_turn_tokens(&after_result).is_empty());
3421 }
3422
3423 #[test]
3424 fn the_last_stream_record_is_the_last_one_with_tokens() {
3425 let events = vec![
3426 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3427 ev(1, generated(&[2], finish::EOS)),
3428 ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3429 ev(3, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3430 ev(4, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3431 ev(5, EventBody::GenerationFingerprint { digest: 9 }),
3432 ];
3433 assert!(matches!(
3434 Daemon::last_stream_record(&events),
3435 Some(EventBody::Generated { finish: finish::EOS, .. })
3436 ));
3437 assert!(Daemon::last_stream_record(&events[2..]).is_none());
3438 }
3439}
3440
3441fn first_registrable<E>(
3442 candidates: &[String],
3443 auto: bool,
3444 mut register: impl FnMut(&str) -> Result<(), E>,
3445 mut refused: impl FnMut(&str, &E, Option<&str>),
3446) -> Result<Option<String>, E> {
3447 for (i, c) in candidates.iter().enumerate() {
3448 match register(c) {
3449 Ok(()) => return Ok(Some(c.clone())),
3450 Err(e) if auto => refused(c, &e, candidates.get(i + 1).map(String::as_str)),
3451 Err(e) => return Err(e),
3452 }
3453 }
3454 Ok(None)
3455}
3456
3457#[cfg(test)]
3458mod speculate_fallback_tests {
3459 use super::first_registrable;
3460
3461 fn strs(v: &[&str]) -> Vec<String> {
3462 v.iter().map(|s| s.to_string()).collect()
3463 }
3464
3465 #[test]
3466 fn auto_tries_each_candidate_in_order_until_one_registers() {
3467 let mut tried = Vec::new();
3468 let mut reported = Vec::new();
3469 let picked = first_registrable(
3470 &strs(&["dspark:a", "dflash:b", "eagle3:c"]),
3471 true,
3472 |s| {
3473 tried.push(s.to_string());
3474 if s == "dflash:b" { Ok(()) } else { Err(format!("no {s}")) }
3475 },
3476 |s, e, next| reported.push((s.to_string(), e.clone(), next.map(str::to_string))),
3477 )
3478 .unwrap();
3479 assert_eq!(picked.as_deref(), Some("dflash:b"));
3480 assert_eq!(tried, ["dspark:a", "dflash:b"]);
3481 assert_eq!(reported, [("dspark:a".to_string(), "no dspark:a".to_string(), Some("dflash:b".to_string()))]);
3482 }
3483
3484 #[test]
3485 fn auto_with_every_candidate_refused_serves_plainly() {
3486 let mut last_next = Some("x".to_string());
3487 let picked = first_registrable(
3488 &strs(&["dspark:a", "dflash:b"]),
3489 true,
3490 |_| Err::<(), _>("refused"),
3491 |_, _, next| last_next = next.map(str::to_string),
3492 )
3493 .unwrap();
3494 assert_eq!(picked, None);
3495 assert_eq!(last_next, None, "the last refusal says it is decoding plainly");
3496 }
3497
3498 #[test]
3499 fn an_explicit_strategy_fails_on_the_first_refusal() {
3500 let mut reported = 0;
3501 let r = first_registrable(
3502 &strs(&["dspark:a", "dflash:b"]),
3503 false,
3504 |s| if s == "dspark:a" { Err("refused") } else { Ok(()) },
3505 |_, _, _| reported += 1,
3506 );
3507 assert_eq!(r, Err("refused"));
3508 assert_eq!(reported, 0);
3509 }
3510}
3511
3512#[cfg(test)]
3513mod latency_tests {
3514 use super::CancelSet;
3515 use std::time::Duration;
3516
3517 #[test]
3518 fn a_latency_request_raises_the_word_and_holds_it_for_a_while() {
3519 let ctl = superfluid_shm::ControlSegment::create().unwrap();
3520 let worker = superfluid_shm::ControlSegment::attach(
3521 superfluid_shm::ShmSegment::from_fd(
3522 {
3523 use std::os::fd::FromRawFd;
3524 unsafe { std::os::fd::OwnedFd::from_raw_fd(libc::dup(ctl.raw_fd())) }
3526 },
3527 superfluid_shm::CONTROL_BYTES,
3528 )
3529 .unwrap(),
3530 )
3531 .unwrap();
3532 let cancels = CancelSet::default();
3533 cancels.wake_with(std::sync::Arc::new(std::sync::RwLock::new(Some(ctl.signal().unwrap()))));
3534 assert!(!cancels.latency_recent(Duration::from_secs(300)), "nothing has come");
3535 cancels.latency_request();
3536 assert!(worker.latency_wanted() && worker.yield_requested(), "short steps, and the running tick ends at its next");
3537 assert!(cancels.latency_recent(Duration::from_secs(300)));
3538 assert!(!cancels.latency_recent(Duration::ZERO), "past the hold");
3539 }
3540
3541 #[test]
3542 fn a_completion_asks_the_running_tick_to_end_as_a_chat_does() {
3543 let host = crate::EngineHost::spawn(|| (superfluid_engine::MockEngine::new(superfluid_engine::EngineConfig::default()), None)).unwrap();
3546 let daemon = crate::Daemon::new(crate::SessionStore::ephemeral(), host, Box::new(crate::MockCodec), 2);
3547 assert!(!daemon.cancels.latency_recent(Duration::from_secs(300)));
3548 daemon.generate_tokens(vec![1, 2, 3], crate::GenParams::default(), 4, 0).unwrap();
3549 assert!(daemon.cancels.latency_recent(Duration::from_secs(300)), "a completion is latency-sensitive");
3550 }
3551}