1pub(crate) use crate::generation_output::*;
4use crate::wal::{channel, role, EventBody};
5use crate::ModelSamplingDefaults;
6use crate::{Daemon, DaemonError, GenParams};
7use superfluid_abi::finish;
8use serde::Deserialize;
9use std::sync::Arc;
10
11pub(crate) const DEFAULT_MAX_TOKENS: u32 = 2048;
12
13#[derive(Clone, Copy)]
14pub struct TokenLimits {
15 pub default_max_tokens: Option<u32>,
16 pub max_context: u32,
17}
18
19impl TokenLimits {
20 pub fn resolve(&self, explicit: Option<u32>, prompt_tokens: impl FnOnce() -> u32) -> u32 {
21 explicit
22 .or(self.default_max_tokens)
23 .unwrap_or_else(|| self.max_context.saturating_sub(prompt_tokens()).max(1))
24 }
25}
26pub(crate) const DEFAULT_MAX_CONTEXT: u32 = 8192;
27
28#[derive(Clone, Copy)]
29pub(crate) struct RequestPolicy {
30 pub(crate) limits: TokenLimits,
31 pub(crate) sampling: SamplingOverrides,
32 pub(crate) qos: RequestQos,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct RequestQos {
37 pub class: u8,
38 pub batch_invariant: bool,
39}
40
41impl RequestQos {
42 pub const AGENT: RequestQos = RequestQos {
43 class: crate::qos::FOREGROUND_AGENT,
44 batch_invariant: false,
45 };
46
47 pub(crate) fn apply(self, daemon: &Daemon, session: u64) -> Result<(), DaemonError> {
48 let held = {
51 let store = daemon.store();
52 let store = store.lock().expect("store");
53 let s = store.session(session)?;
54 RequestQos { class: s.qos_class, batch_invariant: s.batch_invariant }
55 };
56 if self != held {
57 daemon.set_qos(session, self.class, self.batch_invariant)?;
58 }
59 Ok(())
60 }
61}
62
63impl Default for RequestQos {
64 fn default() -> RequestQos {
65 RequestQos::AGENT
66 }
67}
68
69#[derive(Debug, Clone, Copy, Default, PartialEq)]
70pub struct SamplingOverrides {
71 pub temperature: Option<f32>,
72 pub top_p: Option<f32>,
73 pub top_k: Option<u32>,
74 pub min_p: Option<f32>,
75 pub repeat_penalty: Option<f32>,
76 pub spec_max_temperature: Option<f32>,
77 pub fallback: Option<SamplingFallback>,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct SamplingFallback {
86 pub temperature: f32,
87 pub repeat_penalty: f32,
88 pub top_p: f32,
89 pub top_k: u32,
90 pub min_p: f32,
91 pub frequency_penalty_as_repeat: bool,
95}
96
97impl SamplingOverrides {
98 pub fn for_daemon(&self, daemon: &Daemon) -> Self {
99 let mut o = *self;
100 if o.spec_max_temperature.is_some() && daemon.speculation().is_none() {
101 o.spec_max_temperature = None;
102 }
103 o
104 }
105
106 pub fn truncates(&self) -> bool {
107 self.top_p.is_some() || self.top_k.is_some() || self.min_p.is_some()
108 }
109}
110
111#[derive(Deserialize, Default)]
112pub(crate) struct ChatRequest {
113 pub(crate) model: Option<String>,
114 #[serde(default)]
115 pub(crate) messages: Vec<ChatMessage>,
116 #[serde(default)]
117 pub(crate) tools: Vec<serde_json::Value>,
118 #[serde(default)]
119 pub(crate) tool_choice: Option<serde_json::Value>,
120 #[serde(default)]
121 pub(crate) stream: bool,
122 #[serde(default)]
123 pub(crate) stream_options: Option<StreamOptions>,
124 pub(crate) max_completion_tokens: Option<u32>,
125 pub(crate) max_tokens: Option<u32>,
126 pub(crate) temperature: Option<f32>,
127 pub(crate) top_p: Option<f32>,
128 pub(crate) top_k: Option<u32>,
129 pub(crate) seed: Option<u64>,
130 pub(crate) presence_penalty: Option<f32>,
131 pub(crate) frequency_penalty: Option<f32>,
132 #[serde(alias = "repetition_penalty")]
133 pub(crate) repeat_penalty: Option<f32>,
134 pub(crate) min_p: Option<f32>,
135 pub(crate) n: Option<u32>,
136 #[serde(default)]
137 pub(crate) ignore_eos: bool,
138 #[serde(default)]
139 pub(crate) stop: StopField,
140 pub(crate) response_format: Option<serde_json::Value>,
141 pub(crate) logit_bias: Option<std::collections::HashMap<String, f32>>,
142 pub(crate) logprobs: Option<bool>,
143 pub(crate) top_logprobs: Option<u32>,
144 pub(crate) chat_template_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
145 pub(crate) enable_thinking: Option<bool>,
146 pub(crate) reasoning_effort: Option<String>,
147 #[serde(skip)]
150 pub(crate) continues: Option<Continuation>,
151 #[serde(skip)]
154 pub(crate) route: Option<&'static str>,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub(crate) struct Continuation {
160 pub(crate) session: u64,
161 pub(crate) at_event: u64,
162}
163
164impl ChatRequest {
165 pub(crate) fn route(&self) -> &'static str {
166 self.route.unwrap_or("POST /v1/chat/completions")
167 }
168
169 pub(crate) fn thinking(&self) -> Option<bool> {
170 self.chat_template_kwargs
171 .as_ref()
172 .and_then(|m| m.get("enable_thinking"))
173 .and_then(|v| v.as_bool())
174 .or(self.enable_thinking)
175 }
176
177 pub(crate) fn template_kwargs(&self) -> serde_json::Map<String, serde_json::Value> {
178 let mut m = self.chat_template_kwargs.clone().unwrap_or_default();
179 if let Some(t) = self.enable_thinking {
180 m.entry("enable_thinking".to_string())
181 .or_insert(serde_json::Value::Bool(t));
182 }
183 if let Some(e) = &self.reasoning_effort {
184 m.entry("reasoning_effort".to_string())
185 .or_insert(serde_json::Value::String(e.clone()));
186 }
187 m
188 }
189}
190
191pub(crate) fn schema_of(rf: &Option<serde_json::Value>) -> Option<String> {
192 let rf = rf.as_ref()?;
193 match rf.get("type").and_then(|t| t.as_str()) {
194 Some("json_object") => Some(r#"{"type":"object"}"#.to_string()),
195 Some("json_schema") => rf
196 .get("json_schema")
197 .and_then(|js| js.get("schema").or(Some(js)))
198 .map(|s| s.to_string()),
199 _ => None,
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Default)]
204pub(crate) enum ToolChoice {
205 #[default]
206 Auto,
207 None,
208 Required,
209 Function(String),
210}
211
212impl ToolChoice {
213 pub(crate) fn parse(v: &Option<serde_json::Value>) -> ToolChoice {
214 let Some(v) = v else { return ToolChoice::Auto };
215 if let Some(s) = v.as_str() {
216 return match s {
217 "none" => ToolChoice::None,
218 "required" => ToolChoice::Required,
219 _ => ToolChoice::Auto,
220 };
221 }
222 let name = v
223 .get("function")
224 .and_then(|f| f.get("name"))
225 .or_else(|| v.get("name"))
226 .and_then(|n| n.as_str());
227 match name {
228 Some(n) if !n.is_empty() => ToolChoice::Function(n.to_string()),
229 _ => ToolChoice::Auto,
230 }
231 }
232
233 pub(crate) fn forces_call(&self) -> bool {
234 matches!(self, ToolChoice::Required | ToolChoice::Function(_))
235 }
236
237 fn declares_tools(&self) -> bool {
238 !matches!(self, ToolChoice::None)
239 }
240
241 fn constrain_to<'a>(
242 &self,
243 tools: &'a [serde_json::Value],
244 ) -> Option<Vec<&'a serde_json::Value>> {
245 match self {
246 ToolChoice::Auto | ToolChoice::None => None,
247 ToolChoice::Required => Some(tools.iter().collect()),
248 ToolChoice::Function(name) => {
249 let picked: Vec<&serde_json::Value> = tools
250 .iter()
251 .filter(|t| {
252 let f = t.get("function").unwrap_or(*t);
253 f.get("name").and_then(|n| n.as_str()) == Some(name.as_str())
254 })
255 .collect();
256 (!picked.is_empty()).then_some(picked)
257 }
258 }
259 }
260}
261
262pub(crate) struct RequestHandles<'a> {
263 daemon: &'a Daemon,
264 grammar: Option<u32>,
265 bias: Option<u32>,
266}
267
268impl<'a> RequestHandles<'a> {
269 pub(crate) fn new(daemon: &'a Daemon, grammar: Option<u32>, bias: Option<u32>) -> Self {
270 RequestHandles {
271 daemon,
272 grammar,
273 bias,
274 }
275 }
276
277 pub(crate) fn apply(&self, extras: &mut crate::scheduler::GenExtras) {
278 if let Some(h) = self.grammar {
279 extras.grammar_handle = h;
280 }
281 if let Some(h) = self.bias {
282 extras.logit_bias_handle = h;
283 }
284 }
285}
286
287impl Drop for RequestHandles<'_> {
288 fn drop(&mut self) {
289 if let Some(h) = self.grammar.take() {
290 self.daemon.grammar_free(h);
291 }
292 if let Some(h) = self.bias.take() {
293 self.daemon.logit_bias_free(h);
294 }
295 }
296}
297
298pub(crate) fn grammar_handle_for(
299 daemon: &Daemon,
300 req: &ChatRequest,
301 choice: &ToolChoice,
302) -> Result<Option<u32>, DaemonError> {
303 if let ToolChoice::Function(name) = choice {
304 let declared = req.tools.iter().any(|t| {
305 let f = t.get("function").unwrap_or(t);
306 f.get("name").and_then(|n| n.as_str()) == Some(name.as_str())
307 });
308 if !declared {
309 return Err(DaemonError::Constraint(format!(
310 "tool_choice names function {name:?}, which is not among the declared tools"
311 )));
312 }
313 }
314 if !req.tools.is_empty() && *choice != ToolChoice::None {
315 let jsons: Vec<String> = match choice.constrain_to(&req.tools) {
316 Some(tools) => tools.iter().map(|t| t.to_string()).collect(),
317 None => req.tools.iter().map(|t| t.to_string()).collect(),
318 };
319 if let Some(tag) = daemon.tool_structural_tag(&jsons, choice.forces_call()) {
320 if let Ok(h) = daemon.grammar_create_structural(&tag) {
321 if h != 0 {
322 return Ok(Some(h));
323 }
324 }
325 }
326 }
327 if let Some(tools) = choice.constrain_to(&req.tools) {
328 if tools.is_empty() {
329 return Err(DaemonError::Constraint(
330 "tool_choice demands a call but the request declares no tools".to_string(),
331 ));
332 }
333 let jsons: Vec<String> = tools.iter().map(|t| t.to_string()).collect();
334 if let Some(schema) = daemon.tool_call_grammar(&jsons) {
335 if let Ok(h) = daemon.grammar_create(&schema) {
336 if h != 0 {
337 return Ok(Some(h));
338 }
339 }
340 }
341 return Err(DaemonError::Constraint(
342 "tool_choice demands a call, but no tool-call grammar could be compiled for this \
343 model's dialect on this engine"
344 .to_string(),
345 ));
346 }
347 match schema_of(&req.response_format) {
348 None if req
349 .response_format
350 .as_ref()
351 .and_then(|rf| rf.get("type"))
352 .and_then(|t| t.as_str())
353 == Some("json_schema") =>
354 {
355 Err(DaemonError::Constraint(
356 "response_format type json_schema requires a json_schema.schema block".to_string(),
357 ))
358 }
359 None => Ok(None),
360 Some(s) => {
361 if let Some(tag) = daemon.response_format_tag(&s) {
362 return match daemon.grammar_create_structural(&tag) {
363 Ok(h) if h != 0 => Ok(Some(h)),
364 _ => Err(DaemonError::Constraint(
365 "response_format grammar did not compile in this model's answer frame on this engine"
366 .to_string(),
367 )),
368 };
369 }
370 match daemon.grammar_create(&s) {
371 Ok(h) if h != 0 => Ok(Some(h)),
372 Ok(_) => Err(DaemonError::Constraint(
373 "response_format grammar did not compile on this engine (schema rejected or \
374 constrained decoding unavailable in this build)"
375 .to_string(),
376 )),
377 Err(e) => Err(DaemonError::Constraint(format!(
378 "response_format grammar could not be created: {e}"
379 ))),
380 }
381 }
382 }
383}
384
385#[derive(Deserialize, Default, Clone)]
386#[serde(untagged)]
387pub(crate) enum StopField {
388 #[default]
389 None,
390 One(String),
391 Many(Vec<String>),
392}
393
394impl StopField {
395 pub(crate) fn list(&self) -> Vec<String> {
396 match self {
397 StopField::None => Vec::new(),
398 StopField::One(s) => vec![s.clone()],
399 StopField::Many(v) => v.iter().filter(|s| !s.is_empty()).cloned().collect(),
400 }
401 }
402}
403
404fn stop_prefix_holdback(text: &str, stops: &[String]) -> usize {
405 let mut hold = 0usize;
406 for s in stops {
407 if s.is_empty() {
408 continue;
409 }
410 let max = s.len().saturating_sub(1).min(text.len());
411 for k in (1..=max).rev() {
412 if !text.is_char_boundary(text.len() - k) || !s.is_char_boundary(k) {
413 continue;
414 }
415 if text.ends_with(&s[..k]) {
416 hold = hold.max(k);
417 break;
418 }
419 }
420 }
421 hold
422}
423
424pub(crate) fn truncate_at_stop(text: &str, stops: &[String]) -> (String, bool) {
425 let mut cut = None;
426 for s in stops {
427 if let Some(i) = text.find(s.as_str()) {
428 cut = Some(cut.map_or(i, |c: usize| c.min(i)));
429 }
430 }
431 match cut {
432 Some(i) => (text[..i].to_string(), true),
433 None => (text.to_string(), false),
434 }
435}
436
437#[cfg(test)]
438mod penalty_alias_tests {
439 use super::{
440 advertised_generation_defaults, draw_seed_if_unseeded, params_of_for, repeat_penalty_for,
441 ChatRequest, CompletionRequest, SamplingOverrides, DEFAULT_REPEAT_PENALTY, DEFAULT_TOP_K,
442 };
443 use crate::GenParams;
444 use crate::ModelSamplingDefaults;
445
446 #[test]
447 fn unseeded_sampled_requests_draw_distinct_seeds() {
448 let m = ModelSamplingDefaults::default();
449 let op = SamplingOverrides::default();
450 let mut a = params_of_for(Some(0.7), None, None, None, None, &m, &op);
451 let mut b = params_of_for(Some(0.7), None, None, None, None, &m, &op);
452 assert_eq!((a.seed, b.seed), (0, 0), "params_of_for itself never draws");
453 assert!(draw_seed_if_unseeded(&mut a, None));
454 assert!(draw_seed_if_unseeded(&mut b, None));
455 assert_ne!(a.seed, 0);
456 assert_ne!(a.seed, b.seed, "two requests, two streams");
457 let mut greedy = params_of_for(Some(0.0), None, None, None, None, &m, &op);
458 assert!(!draw_seed_if_unseeded(&mut greedy, None));
459 assert_eq!(greedy.seed, 0, "greedy has no use for a seed");
460 let mut explicit = params_of_for(Some(0.7), None, None, None, Some(42), &m, &op);
461 assert!(!draw_seed_if_unseeded(&mut explicit, Some(42)));
462 assert_eq!(explicit.seed, 42, "the client's seed is honoured as is");
463 }
464
465 #[test]
466 fn repetition_penalty_is_an_alias_for_repeat_penalty() {
467 let canonical: ChatRequest =
468 serde_json::from_str(r#"{"messages":[],"repeat_penalty":1.3}"#)
469 .expect("canonical spelling");
470 let alias: ChatRequest =
471 serde_json::from_str(r#"{"messages":[],"repetition_penalty":1.3}"#)
472 .expect("alias spelling");
473 assert_eq!(canonical.repeat_penalty, Some(1.3));
474 assert_eq!(
475 alias.repeat_penalty,
476 Some(1.3),
477 "repetition_penalty was dropped"
478 );
479
480 let c_canon: CompletionRequest =
481 serde_json::from_str(r#"{"prompt":"hi","repeat_penalty":1.3}"#).expect("canonical");
482 let c_alias: CompletionRequest =
483 serde_json::from_str(r#"{"prompt":"hi","repetition_penalty":1.3}"#).expect("alias");
484 assert_eq!(c_canon.repeat_penalty, Some(1.3));
485 assert_eq!(
486 c_alias.repeat_penalty,
487 Some(1.3),
488 "repetition_penalty was dropped on /v1/completions"
489 );
490 }
491
492 fn runtime_only(
493 temperature: Option<f32>,
494 top_p: Option<f32>,
495 top_k: Option<u32>,
496 seed: Option<u64>,
497 ) -> GenParams {
498 params_of_for(
499 temperature,
500 top_p,
501 top_k,
502 None,
503 seed,
504 &ModelSamplingDefaults::default(),
505 &SamplingOverrides::default(),
506 )
507 }
508
509 #[test]
510 fn unspecified_truncation_defaults_to_top_k() {
511 let p = runtime_only(None, None, None, None);
512 assert_eq!(
513 p.top_k, DEFAULT_TOP_K,
514 "neither knob given -> default top_k"
515 );
516 assert_eq!(p.top_p, 1.0, "top_p still reports its nominal default");
517 assert_eq!(p.temperature, 1.0);
518 }
519
520 #[test]
521 fn explicit_sampling_knobs_are_never_overridden() {
522 let explicit_top_p = runtime_only(None, Some(1.0), None, None);
523 assert_eq!(
524 explicit_top_p.top_k, 0,
525 "explicit top_p disables the default"
526 );
527 assert_eq!(runtime_only(None, Some(0.9), None, None).top_k, 0);
528 assert_eq!(
529 runtime_only(None, None, Some(7), None).top_k,
530 7,
531 "explicit top_k wins"
532 );
533 assert_eq!(
534 runtime_only(None, None, Some(0), None).top_k,
535 0,
536 "explicit 0 means disabled"
537 );
538 }
539
540 #[test]
541 fn greedy_gets_no_truncation_default() {
542 assert_eq!(runtime_only(Some(0.0), None, None, None).top_k, 0);
543 }
544
545 #[test]
546 fn advertised_defaults_match_the_effective_ones() {
547 let adv = advertised_generation_defaults(
548 &ModelSamplingDefaults::default(),
549 &SamplingOverrides::default(),
550 );
551 let effective = runtime_only(None, None, None, None);
552 assert_eq!(
553 adv.top_k, effective.top_k,
554 "advertised top_k must be the effective one"
555 );
556 assert_eq!(adv.top_p, effective.top_p);
557 assert_eq!(adv.temperature, effective.temperature);
558 assert_eq!(adv.top_k, DEFAULT_TOP_K);
559 }
560
561 #[test]
562 fn round_tripping_advertised_defaults_is_stable() {
563 let adv = advertised_generation_defaults(
564 &ModelSamplingDefaults::default(),
565 &SamplingOverrides::default(),
566 );
567 let echoed = runtime_only(
568 Some(adv.temperature),
569 Some(adv.top_p),
570 Some(adv.top_k),
571 None,
572 );
573 assert_eq!(echoed.top_k, adv.top_k, "echoed top_k survives");
574 assert_eq!(echoed.top_p, adv.top_p);
575 assert_eq!(echoed.temperature, adv.temperature);
576 }
577
578 #[test]
579 fn model_defaults_beat_the_runtime_fallback() {
580 let qwen38 = ModelSamplingDefaults {
581 temperature: Some(1.0),
582 top_p: Some(0.95),
583 top_k: Some(20),
584 min_p: Some(0.0),
585 do_sample: Some(true),
586 repetition_penalty: None,
587 };
588 let p = params_of_for(
589 None,
590 None,
591 None,
592 None,
593 None,
594 &qwen38,
595 &SamplingOverrides::default(),
596 );
597 assert_eq!(p.top_k, 20, "the model asked for 20, not the fallback 40");
598 assert_eq!(p.top_p, 0.95);
599 assert_eq!(p.temperature, 1.0);
600 }
601
602 #[test]
603 fn caller_beats_the_model_defaults() {
604 let m = ModelSamplingDefaults {
605 top_k: Some(20),
606 top_p: Some(0.95),
607 ..Default::default()
608 };
609 assert_eq!(
610 params_of_for(
611 None,
612 None,
613 Some(5),
614 None,
615 None,
616 &m,
617 &SamplingOverrides::default()
618 )
619 .top_k,
620 5
621 );
622 assert_eq!(
623 params_of_for(
624 None,
625 Some(0.5),
626 None,
627 None,
628 None,
629 &m,
630 &SamplingOverrides::default()
631 )
632 .top_p,
633 0.5
634 );
635 assert_eq!(
636 params_of_for(
637 Some(0.3),
638 None,
639 None,
640 None,
641 None,
642 &m,
643 &SamplingOverrides::default()
644 )
645 .temperature,
646 0.3
647 );
648 }
649
650 #[test]
651 fn empty_model_defaults_keep_the_fallback() {
652 let p = runtime_only(None, None, None, None);
653 assert_eq!(p.top_k, DEFAULT_TOP_K);
654 }
655
656 #[test]
657 fn spec_max_temperature_caps_only_the_bundle_default() {
658 let m = ModelSamplingDefaults {
659 temperature: Some(1.0),
660 ..Default::default()
661 };
662 let op = SamplingOverrides {
663 spec_max_temperature: Some(0.6),
664 ..Default::default()
665 };
666 assert_eq!(
667 params_of_for(None, None, None, None, None, &m, &op).temperature,
668 0.6
669 );
670 assert_eq!(
671 params_of_for(Some(0.9), None, None, None, None, &m, &op).temperature,
672 0.9,
673 "caller wins"
674 );
675 let op_t = SamplingOverrides {
676 temperature: Some(0.8),
677 ..op
678 };
679 assert_eq!(
680 params_of_for(None, None, None, None, None, &m, &op_t).temperature,
681 0.8,
682 "--temperature wins"
683 );
684 let cool = ModelSamplingDefaults {
685 temperature: Some(0.3),
686 ..Default::default()
687 };
688 assert_eq!(
689 params_of_for(None, None, None, None, None, &cool, &op).temperature,
690 0.3
691 );
692 let greedy = ModelSamplingDefaults {
693 do_sample: Some(false),
694 ..Default::default()
695 };
696 assert_eq!(
697 params_of_for(None, None, None, None, None, &greedy, &op).temperature,
698 0.0
699 );
700 assert_eq!(
701 params_of_for(
702 None,
703 None,
704 None,
705 None,
706 None,
707 &ModelSamplingDefaults::default(),
708 &op
709 )
710 .temperature,
711 0.6
712 );
713 assert_eq!(advertised_generation_defaults(&m, &op).temperature, 0.6);
714 }
715
716 #[test]
717 fn do_sample_false_means_greedy() {
718 let m = ModelSamplingDefaults {
719 do_sample: Some(false),
720 ..Default::default()
721 };
722 assert_eq!(
723 params_of_for(
724 None,
725 None,
726 None,
727 None,
728 None,
729 &m,
730 &SamplingOverrides::default()
731 )
732 .temperature,
733 0.0
734 );
735 assert_eq!(
736 params_of_for(
737 Some(0.8),
738 None,
739 None,
740 None,
741 None,
742 &m,
743 &SamplingOverrides::default()
744 )
745 .temperature,
746 0.8
747 );
748 }
749
750 #[test]
751 fn model_top_p_alone_suppresses_the_fallback_top_k() {
752 let m = ModelSamplingDefaults {
753 top_p: Some(0.9),
754 ..Default::default()
755 };
756 let p = params_of_for(
757 None,
758 None,
759 None,
760 None,
761 None,
762 &m,
763 &SamplingOverrides::default(),
764 );
765 assert_eq!(p.top_p, 0.9);
766 assert_eq!(
767 p.top_k, 0,
768 "no fallback top_k when the model already truncates"
769 );
770 }
771
772 #[test]
773 fn explicit_top_p_suppresses_the_model_top_k() {
774 let qwen38 = ModelSamplingDefaults {
775 top_p: Some(0.95),
776 top_k: Some(20),
777 ..Default::default()
778 };
779 let p = params_of_for(
780 None,
781 Some(1.0),
782 None,
783 None,
784 None,
785 &qwen38,
786 &SamplingOverrides::default(),
787 );
788 assert_eq!(p.top_p, 1.0, "caller asked for the full distribution");
789 assert_eq!(p.top_k, 0, "and must not inherit the model's top_k");
790 }
791
792 #[test]
793 fn explicit_top_k_suppresses_the_model_top_p() {
794 let qwen38 = ModelSamplingDefaults {
795 top_p: Some(0.95),
796 top_k: Some(20),
797 ..Default::default()
798 };
799 let p = params_of_for(
800 None,
801 None,
802 Some(5),
803 None,
804 None,
805 &qwen38,
806 &SamplingOverrides::default(),
807 );
808 assert_eq!(p.top_k, 5);
809 assert_eq!(
810 p.top_p, 1.0,
811 "model top_p is set aside once the caller truncates"
812 );
813 }
814
815 #[test]
816 fn model_min_p_alone_is_a_truncation_claim() {
817 let m = ModelSamplingDefaults {
818 min_p: Some(0.05),
819 ..Default::default()
820 };
821 let p = params_of_for(
822 None,
823 None,
824 None,
825 None,
826 None,
827 &m,
828 &SamplingOverrides::default(),
829 );
830 assert_eq!(p.min_p, 0.05);
831 assert_eq!(
832 p.top_k, 0,
833 "no fallback top_k stacked on the model's policy"
834 );
835 }
836
837 #[test]
838 fn advertised_defaults_follow_the_model() {
839 let qwen38 = ModelSamplingDefaults {
840 top_p: Some(0.95),
841 top_k: Some(20),
842 ..Default::default()
843 };
844 let adv = advertised_generation_defaults(&qwen38, &SamplingOverrides::default());
845 assert_eq!(adv.top_k, 20);
846 assert_eq!(adv.top_p, 0.95);
847 let echoed = params_of_for(
848 Some(adv.temperature),
849 Some(adv.top_p),
850 Some(adv.top_k),
851 None,
852 None,
853 &qwen38,
854 &SamplingOverrides::default(),
855 );
856 assert_eq!(
857 echoed.top_k, 20,
858 "echoing the advertised values changes nothing"
859 );
860 assert_eq!(echoed.top_p, 0.95);
861 }
862
863 #[test]
864 fn operator_flags_sit_between_caller_and_bundle() {
865 let qwen38 = ModelSamplingDefaults {
866 temperature: Some(0.6),
867 top_p: Some(0.95),
868 top_k: Some(20),
869 min_p: Some(0.0),
870 do_sample: Some(true),
871 repetition_penalty: None,
872 };
873 let op = SamplingOverrides {
874 top_k: Some(64),
875 ..Default::default()
876 };
877
878 let p = params_of_for(None, None, None, None, None, &qwen38, &op);
879 assert_eq!(p.top_k, 64, "operator top_k overrides the bundle's");
880 assert_eq!(
881 p.top_p, 1.0,
882 "and takes the whole level: the bundle's top_p is set aside"
883 );
884 assert_eq!(
885 p.temperature, 0.6,
886 "temperature is a separate rung and still comes from the bundle"
887 );
888
889 let p = params_of_for(None, None, Some(7), None, None, &qwen38, &op);
890 assert_eq!(p.top_k, 7, "an explicit request value wins over the flag");
891
892 let p = params_of_for(
893 None,
894 None,
895 None,
896 None,
897 None,
898 &ModelSamplingDefaults::default(),
899 &op,
900 );
901 assert_eq!(p.top_k, 64);
902 }
903
904 #[test]
905 fn operator_temperature_precedence() {
906 let greedy_model = ModelSamplingDefaults {
907 do_sample: Some(false),
908 ..Default::default()
909 };
910 let op = SamplingOverrides {
911 temperature: Some(0.9),
912 ..Default::default()
913 };
914 assert_eq!(
915 params_of_for(None, None, None, None, None, &greedy_model, &op).temperature,
916 0.9,
917 "the operator outranks the checkpoint"
918 );
919 assert_eq!(
920 params_of_for(Some(0.0), None, None, None, None, &greedy_model, &op).temperature,
921 0.0,
922 "an explicit request temperature still wins"
923 );
924 }
925
926 #[test]
927 fn an_unknown_penalty_is_omitted_but_a_pinned_one_is_not() {
928 let advertise = |resolved: Option<ModelSamplingDefaults>, op: SamplingOverrides| {
929 (resolved.is_some() || op.repeat_penalty.is_some())
930 .then(|| repeat_penalty_for(None, &op, &resolved.clone().unwrap_or_default()))
931 };
932 let none = SamplingOverrides::default();
933
934 assert_eq!(advertise(None, none), None);
935
936 let op = SamplingOverrides {
937 repeat_penalty: Some(1.2),
938 ..Default::default()
939 };
940 assert_eq!(advertise(None, op), Some(1.2));
941
942 let quiet = ModelSamplingDefaults::default();
943 assert_eq!(advertise(Some(quiet), none), Some(DEFAULT_REPEAT_PENALTY));
944 let opted_out = ModelSamplingDefaults {
945 repetition_penalty: Some(1.0),
946 ..Default::default()
947 };
948 assert_eq!(advertise(Some(opted_out), none), Some(1.0));
949 }
950
951 #[test]
952 fn operator_owned_settings_survive_a_failed_bundle_lookup() {
953 let advertise = |resolved: Option<ModelSamplingDefaults>, op: SamplingOverrides| {
954 let eff = advertised_generation_defaults(&resolved.clone().unwrap_or_default(), &op);
955 let temp = (resolved.is_some() || op.temperature.is_some()).then_some(eff.temperature);
956 let trunc = (resolved.is_some() || op.truncates()).then_some((eff.top_p, eff.top_k));
957 (temp, trunc)
958 };
959
960 assert_eq!(advertise(None, SamplingOverrides::default()), (None, None));
961
962 let op = SamplingOverrides {
963 top_k: Some(64),
964 ..Default::default()
965 };
966 assert_eq!(advertise(None, op), (None, Some((1.0, 64))));
967
968 let op = SamplingOverrides {
969 temperature: Some(0.4),
970 ..Default::default()
971 };
972 assert_eq!(advertise(None, op), (Some(0.4), None));
973
974 let qwen38 = ModelSamplingDefaults {
975 temperature: Some(0.6),
976 top_p: Some(0.95),
977 top_k: Some(20),
978 min_p: Some(0.0),
979 do_sample: Some(true),
980 repetition_penalty: None,
981 };
982 let op = SamplingOverrides {
983 top_k: Some(64),
984 ..Default::default()
985 };
986 assert_eq!(advertise(Some(qwen38), op), (Some(0.6), Some((1.0, 64))));
987 }
988
989 #[test]
990 fn repeat_penalty_resolves_caller_then_operator_then_bundle_then_default() {
991 let none = SamplingOverrides::default();
992 let no_model = ModelSamplingDefaults::default();
993
994 assert_eq!(
995 repeat_penalty_for(None, &none, &no_model),
996 DEFAULT_REPEAT_PENALTY
997 );
998 assert_eq!(repeat_penalty_for(Some(1.2), &none, &no_model), 1.2);
999
1000 let model = ModelSamplingDefaults {
1001 repetition_penalty: Some(1.1),
1002 ..Default::default()
1003 };
1004 assert_eq!(
1005 repeat_penalty_for(None, &none, &model),
1006 1.1,
1007 "bundle beats the runtime default"
1008 );
1009
1010 let opted_out = ModelSamplingDefaults {
1011 repetition_penalty: Some(1.0),
1012 ..Default::default()
1013 };
1014 assert_eq!(repeat_penalty_for(None, &none, &opted_out), 1.0);
1015
1016 let op = SamplingOverrides {
1017 repeat_penalty: Some(1.2),
1018 ..Default::default()
1019 };
1020 assert_eq!(
1021 repeat_penalty_for(None, &op, &model),
1022 1.2,
1023 "flag beats the bundle"
1024 );
1025 assert_eq!(
1026 repeat_penalty_for(Some(1.0), &op, &model),
1027 1.0,
1028 "caller's 1.0 opts out"
1029 );
1030 }
1031
1032 #[test]
1033 fn echoing_advertised_min_p_is_stable() {
1034 let model = ModelSamplingDefaults {
1035 top_p: Some(0.95),
1036 top_k: Some(20),
1037 min_p: Some(0.05),
1038 ..Default::default()
1039 };
1040 let adv = advertised_generation_defaults(&model, &SamplingOverrides::default());
1041 assert_eq!(adv.min_p, 0.05, "advertised min_p comes from the model");
1042 let echoed = params_of_for(
1043 Some(adv.temperature),
1044 Some(adv.top_p),
1045 Some(adv.top_k),
1046 Some(adv.min_p),
1047 None,
1048 &model,
1049 &SamplingOverrides::default(),
1050 );
1051 assert_eq!(echoed.min_p, 0.05, "echoed min_p survives the round trip");
1052 assert_eq!(echoed.top_p, 0.95);
1053 assert_eq!(echoed.top_k, 20);
1054 }
1055}
1056
1057#[cfg(test)]
1058mod stop_tests {
1059 use super::{stop_prefix_holdback, truncate_at_stop};
1060
1061 fn stops(s: &[&str]) -> Vec<String> {
1062 s.iter().map(|x| x.to_string()).collect()
1063 }
1064
1065 #[test]
1066 fn holdback_is_the_longest_stop_prefix_at_the_tail() {
1067 let st = stops(&["END", "<|eot|>"]);
1068 assert_eq!(stop_prefix_holdback("hello", &st), 0);
1069 assert_eq!(stop_prefix_holdback("hello E", &st), 1);
1070 assert_eq!(stop_prefix_holdback("hello EN", &st), 2);
1071 assert_eq!(stop_prefix_holdback("hello END", &st), 0);
1072 assert_eq!(stop_prefix_holdback("x <|eot", &st), 5);
1073 assert_eq!(stop_prefix_holdback("a <|e", &stops(&["<|eot|>", "e"])), 3);
1074 assert_eq!(
1075 stop_prefix_holdback("héllo —", &stops(&["— fin"])),
1076 "—".len()
1077 );
1078 assert_eq!(stop_prefix_holdback("abc", &stops(&[""])), 0);
1079 }
1080
1081 #[test]
1082 fn delta_stream_never_emits_a_stop_prefix_that_the_next_delta_completes() {
1083 let st = stops(&["END"]);
1084 let mut acc = String::new();
1085 let mut sent = 0usize;
1086 let mut emitted = String::new();
1087 for delta in ["hello E", "ND world"] {
1088 acc.push_str(delta);
1089 let (mut t, hit) = truncate_at_stop(&acc, &st);
1090 if !hit {
1091 let keep = t.len() - stop_prefix_holdback(&t, &st);
1092 t.truncate(keep);
1093 }
1094 emitted.push_str(t.get(sent..).unwrap_or(""));
1095 sent = sent.max(t.len());
1096 if hit {
1097 break;
1098 }
1099 }
1100 assert_eq!(emitted, "hello ");
1101 }
1102}
1103
1104#[derive(Clone, Copy, Default)]
1105pub(crate) struct DecodeSpan {
1106 first: Option<std::time::Instant>,
1107 last: Option<std::time::Instant>,
1108 first_count: u64,
1109 last_count: u64,
1110 tokens: u64,
1111 elapsed: std::time::Duration,
1112}
1113
1114impl DecodeSpan {
1115 pub(crate) fn stamp(&mut self, produced: u64) {
1116 self.stamp_at(std::time::Instant::now(), produced);
1117 }
1118
1119 fn stamp_at(&mut self, now: std::time::Instant, produced: u64) {
1120 if self.first.is_none() {
1121 self.first = Some(now);
1122 self.first_count = produced;
1123 }
1124 self.last = Some(now);
1125 self.last_count = produced.max(self.last_count);
1126 }
1127
1128 pub(crate) fn first_token(&self) -> Option<std::time::Instant> {
1129 self.first
1130 }
1131
1132 pub(crate) fn finish(&mut self) {
1133 if let (Some(f), Some(l)) = (self.first, self.last) {
1134 self.tokens += self.last_count.saturating_sub(self.first_count);
1135 self.elapsed += l.saturating_duration_since(f);
1136 }
1137 self.first = None;
1138 self.last = None;
1139 self.first_count = 0;
1140 self.last_count = 0;
1141 }
1142
1143 fn measured(&self) -> (u64, Option<std::time::Duration>) {
1144 if self.elapsed == std::time::Duration::ZERO {
1145 (0, None)
1146 } else {
1147 (self.tokens, Some(self.elapsed))
1148 }
1149 }
1150}
1151
1152#[derive(Clone, Copy)]
1153pub(crate) struct GenTiming {
1154 pub t0: std::time::Instant,
1155 pub first: Option<std::time::Instant>,
1156 pub decode: DecodeSpan,
1157}
1158
1159fn fim_log_cached(c: &crate::Completion) -> u64 {
1160 if c.cached {
1161 c.prompt_tokens as u64
1162 } else {
1163 0
1164 }
1165}
1166
1167fn prefill_rate(prompt: u64, cached: u64, ttft: Option<std::time::Duration>) -> String {
1168 let prefilled = prompt - cached.min(prompt);
1169 ttft.filter(|d| d.as_secs_f64() > 0.0 && prefilled > 0)
1170 .map(|d| format!("{:.0}", prefilled as f64 / d.as_secs_f64()))
1171 .unwrap_or_else(|| "-".into())
1172}
1173
1174fn decode_rate(tokens: u64, span: Option<std::time::Duration>) -> String {
1175 span.filter(|d| tokens > 0 && d.as_millis() as u64 >= tokens)
1176 .map(|d| format!("{:.1}", tokens as f64 / d.as_secs_f64()))
1177 .unwrap_or_else(|| "-".into())
1178}
1179
1180pub(crate) fn access_log(
1181 route: &str,
1182 model: &str,
1183 prompt: u64,
1184 cached: u64,
1185 completion: u64,
1186 timing: GenTiming,
1187 finish: &str,
1188) {
1189 let GenTiming { t0, first, decode } = timing;
1190 let total_ms = t0.elapsed().as_millis();
1191 let ttft = first.map(|f| f.saturating_duration_since(t0));
1192 let (decode_tokens, decode_span) = decode.measured();
1193 let prefill = prefill_rate(prompt, cached, ttft);
1194 let decode = decode_rate(decode_tokens, decode_span);
1195 tracing::info!(
1196 "<-- 200 {route} model={model} prompt={prompt} cached={cached} completion={completion} ttft={}ms prefill={prefill} tok/s decode={decode} tok/s total={total_ms}ms finish={finish}",
1197 ttft.map(|d| d.as_millis()).unwrap_or(0),
1198 );
1199}
1200
1201pub(crate) fn extras_of(
1202 presence: Option<f32>,
1203 frequency: Option<f32>,
1204 repeat: Option<f32>,
1205 op: &SamplingOverrides,
1206 model: &ModelSamplingDefaults,
1207) -> crate::scheduler::GenExtras {
1208 let mut frequency_penalty = frequency.unwrap_or(0.0);
1209 let mut repeat_penalty = repeat_penalty_for(repeat, op, model);
1210 if op.fallback.is_some_and(|f| f.frequency_penalty_as_repeat)
1211 && repeat.is_none()
1212 && op.repeat_penalty.is_none()
1213 {
1214 if presence.is_some() {
1215 repeat_penalty = 1.0;
1216 } else if let Some(f) = frequency {
1217 repeat_penalty = 1.0 + f;
1218 frequency_penalty = 0.0;
1219 }
1220 }
1221 crate::scheduler::GenExtras {
1222 ephemeral: true,
1223 presence_penalty: presence.unwrap_or(0.0),
1224 frequency_penalty,
1225 repeat_penalty,
1226 grammar_handle: 0,
1227 logit_bias_handle: 0,
1228 want_logprobs: false,
1229 top_logprobs: 0,
1230 wants_deltas: false,
1231 open_channel: 0,
1232 thinking: None,
1233 ignore_eos: false,
1234 seed_drawn: false,
1235 tool_schemas: None,
1236 }
1237}
1238
1239pub(crate) fn logit_bias_handle_of(
1240 daemon: &Daemon,
1241 map: &Option<std::collections::HashMap<String, f32>>,
1242) -> Option<u32> {
1243 let map = map.as_ref()?;
1244 if map.is_empty() {
1245 return None;
1246 }
1247 let mut tokens = Vec::with_capacity(map.len());
1248 let mut values = Vec::with_capacity(map.len());
1249 for (k, v) in map {
1250 if let Ok(t) = k.parse::<i32>() {
1251 tokens.push(t);
1252 values.push(*v);
1253 }
1254 }
1255 if tokens.is_empty() {
1256 return None;
1257 }
1258 daemon.logit_bias_create(&tokens, &values).ok()
1259}
1260
1261#[derive(Debug, Default, Deserialize)]
1262pub(crate) struct StreamOptions {
1263 #[serde(default)]
1264 #[allow(dead_code)]
1265 pub(crate) include_usage: bool,
1266 #[serde(default)]
1267 pub(crate) continuous_usage_stats: bool,
1268}
1269
1270#[derive(Deserialize)]
1271pub(crate) struct CompletionRequest {
1272 pub(crate) model: Option<String>,
1273 #[serde(default)]
1274 pub(crate) ignore_eos: bool,
1275 #[serde(default)]
1276 pub(crate) prompt: String,
1277 #[serde(default)]
1278 pub(crate) stream: bool,
1279 #[serde(default)]
1280 pub(crate) echo: bool,
1281 pub(crate) max_tokens: Option<u32>,
1282 pub(crate) temperature: Option<f32>,
1283 pub(crate) top_p: Option<f32>,
1284 pub(crate) top_k: Option<u32>,
1285 pub(crate) seed: Option<u64>,
1286 pub(crate) presence_penalty: Option<f32>,
1287 pub(crate) frequency_penalty: Option<f32>,
1288 #[serde(alias = "repetition_penalty")]
1289 pub(crate) repeat_penalty: Option<f32>,
1290 pub(crate) min_p: Option<f32>,
1291 pub(crate) logprobs: Option<u32>,
1292 pub(crate) suffix: Option<String>,
1293 pub(crate) fim_mode: Option<String>,
1294}
1295
1296pub(crate) const DEFAULT_TOP_K: u32 = 40;
1297
1298pub(crate) const DEFAULT_REPEAT_PENALTY: f32 = 1.0;
1299
1300#[cfg(test)]
1301mod embedding_sampling_tests {
1302 use super::*;
1303
1304 fn policy() -> SamplingOverrides {
1305 SamplingOverrides {
1306 fallback: Some(SamplingFallback {
1307 temperature: 0.0, repeat_penalty: 1.07,
1308 top_p: 0.82, top_k: 17, min_p: 0.03,
1309 frequency_penalty_as_repeat: true,
1310 }),
1311 ..Default::default()
1312 }
1313 }
1314
1315 #[test]
1316 fn omitted_parameters_and_props_share_embedding_defaults() {
1317 let m = ModelSamplingDefaults::default();
1318 let op = policy();
1319 let p = advertised_generation_defaults(&m, &op);
1320 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p), (0.0, 0.82, 17, 0.03));
1321 assert_eq!(repeat_penalty_for(None, &op, &m), 1.07);
1322 let original = advertised_generation_defaults(&m, &SamplingOverrides::default());
1323 assert_eq!((original.temperature, original.top_p, original.top_k, original.min_p), (1.0, 1.0, 40, 0.0));
1324 assert_eq!(repeat_penalty_for(None, &SamplingOverrides::default(), &m), 1.0);
1325 }
1326
1327 #[test]
1328 fn model_truncation_wins_as_a_set_but_not_model_temperature_or_repeat() {
1329 for model in [
1330 ModelSamplingDefaults { top_p: Some(0.95), ..Default::default() },
1331 ModelSamplingDefaults { top_k: Some(9), ..Default::default() },
1332 ModelSamplingDefaults { min_p: Some(0.1), ..Default::default() },
1333 ] {
1334 let model = ModelSamplingDefaults { temperature: Some(0.7), repetition_penalty: Some(1.2), ..model };
1335 let p = advertised_generation_defaults(&model, &policy());
1336 assert_eq!(p.temperature, 0.0);
1337 assert_eq!((p.top_p, p.top_k, p.min_p), (model.top_p.unwrap_or(1.0), model.top_k.unwrap_or(0), model.min_p.unwrap_or(0.0)));
1338 assert_eq!(repeat_penalty_for(None, &policy(), &model), 1.07);
1339 }
1340 }
1341
1342 #[test]
1343 fn caller_and_operator_values_override_embedding_without_merging_truncation() {
1344 let m = ModelSamplingDefaults { top_k: Some(90), ..Default::default() };
1345 let op = SamplingOverrides { temperature: Some(0.6), top_k: Some(12), repeat_penalty: Some(1.3), ..policy() };
1346 let p = advertised_generation_defaults(&m, &op);
1347 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p), (0.6, 1.0, 12, 0.0));
1348 assert_eq!(repeat_penalty_for(None, &op, &m), 1.3);
1349 let p = params_of_for(Some(0.2), Some(1.0), None, None, Some(42), &m, &op);
1350 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p, p.seed), (0.2, 1.0, 0, 0.0, 42));
1351 assert_eq!(repeat_penalty_for(Some(1.0), &op, &m), 1.0);
1352 }
1353
1354 #[test]
1355 fn penalty_mapping_is_opt_in_and_respects_explicit_parameters() {
1356 let m = ModelSamplingDefaults::default();
1357 for (presence, frequency, repeat, expected) in [
1358 (None, None, None, (0.0, 0.0, 1.07)),
1359 (None, Some(0.2), None, (0.0, 0.0, 1.2)),
1360 (None, Some(0.0), None, (0.0, 0.0, 1.0)),
1361 (Some(0.0), Some(0.2), None, (0.0, 0.2, 1.0)),
1362 (Some(0.3), None, None, (0.3, 0.0, 1.0)),
1363 (None, Some(0.2), Some(1.1), (0.0, 0.2, 1.1)),
1364 ] {
1365 let e = extras_of(presence, frequency, repeat, &policy(), &m);
1366 assert_eq!((e.presence_penalty, e.frequency_penalty, e.repeat_penalty), expected);
1367 }
1368 let e = extras_of(None, Some(0.2), None, &SamplingOverrides::default(), &m);
1369 assert_eq!((e.frequency_penalty, e.repeat_penalty), (0.2, 1.0));
1370 }
1371}
1372
1373pub(crate) fn advertised_generation_defaults(
1374 model: &ModelSamplingDefaults,
1375 op: &SamplingOverrides,
1376) -> GenParams {
1377 params_of_for(None, None, None, None, None, model, op)
1378}
1379
1380pub(crate) fn repeat_penalty_for(
1381 caller: Option<f32>,
1382 op: &SamplingOverrides,
1383 model: &ModelSamplingDefaults,
1384) -> f32 {
1385 caller
1386 .or(op.repeat_penalty)
1387 .or(op.fallback.map(|f| f.repeat_penalty))
1388 .or(model.repetition_penalty)
1389 .unwrap_or(DEFAULT_REPEAT_PENALTY)
1390}
1391
1392pub(crate) fn draw_seed_if_unseeded(params: &mut GenParams, client_seed: Option<u64>) -> bool {
1393 if client_seed.is_some() || params.temperature <= 0.0 {
1394 return false;
1395 }
1396 params.seed = fresh_seed();
1397 true
1398}
1399
1400fn fresh_seed() -> u64 {
1401 use std::hash::{BuildHasher, Hasher};
1402 let h = std::collections::hash_map::RandomState::new()
1403 .build_hasher()
1404 .finish();
1405 if h == 0 {
1406 1
1407 } else {
1408 h
1409 }
1410}
1411
1412pub(crate) fn params_of_for(
1413 temperature: Option<f32>,
1414 top_p: Option<f32>,
1415 top_k: Option<u32>,
1416 min_p: Option<f32>,
1417 seed: Option<u64>,
1418 model: &ModelSamplingDefaults,
1419 op: &SamplingOverrides,
1420) -> GenParams {
1421 let model_temperature = match model.do_sample {
1422 Some(false) => Some(0.0),
1423 _ => model.temperature,
1424 };
1425 let chosen = temperature.is_some() || op.temperature.is_some();
1426 let temperature = temperature
1427 .or(op.temperature)
1428 .or(op.fallback.map(|f| f.temperature))
1429 .or(model_temperature)
1430 .unwrap_or(1.0);
1431 let temperature = match op.spec_max_temperature {
1432 Some(cap) if !chosen => temperature.min(cap),
1433 _ => temperature,
1434 };
1435
1436 let caller_truncates = top_p.is_some() || top_k.is_some() || min_p.is_some();
1437 let model_truncates = model.top_p.is_some() || model.top_k.is_some() || model.min_p.is_some();
1438 let (eff_top_p, eff_top_k, eff_min_p) = if caller_truncates {
1439 (top_p, top_k, min_p)
1440 } else if op.truncates() {
1441 (op.top_p, op.top_k, op.min_p)
1442 } else if model_truncates {
1443 (model.top_p, model.top_k, model.min_p)
1444 } else if let Some(f) = op.fallback {
1445 (Some(f.top_p), Some(f.top_k), Some(f.min_p))
1446 } else if temperature > 0.0 {
1447 (None, Some(DEFAULT_TOP_K), None)
1448 } else {
1449 (None, None, None)
1450 };
1451 GenParams {
1452 temperature,
1453 top_p: eff_top_p.unwrap_or(1.0),
1454 min_p: eff_min_p.unwrap_or(0.0),
1455 top_k: eff_top_k.unwrap_or(0),
1456 seed: seed.unwrap_or(0),
1457 }
1458}
1459
1460#[derive(Deserialize, Default)]
1461pub(crate) struct ChatMessage {
1462 pub(crate) role: String,
1463 #[serde(default)]
1464 pub(crate) content: Option<serde_json::Value>,
1465 #[serde(default)]
1466 pub(crate) tool_calls: Vec<ReqToolCall>,
1467 #[serde(default)]
1468 pub(crate) tool_call_id: Option<String>,
1469 #[serde(default)]
1470 pub(crate) name: Option<String>,
1471 #[serde(default)]
1472 pub(crate) reasoning_content: Option<String>,
1473 #[serde(default)]
1474 pub(crate) reasoning: Option<String>,
1475}
1476
1477pub(crate) fn reasoning_of(m: &ChatMessage, role: u32) -> Option<String> {
1478 if role != role::ASSISTANT {
1479 return None;
1480 }
1481 m.reasoning_content
1482 .as_deref()
1483 .or(m.reasoning.as_deref())
1484 .map(str::trim)
1485 .filter(|s| !s.is_empty())
1486 .map(str::to_string)
1487}
1488
1489#[derive(Clone, Copy, PartialEq, Eq)]
1490pub(crate) enum MediaKind {
1491 Image,
1492 Audio,
1493}
1494
1495fn require_media(daemon: &Daemon, kind: MediaKind, param: &str) -> Result<(), DaemonError> {
1496 match kind {
1497 MediaKind::Image => daemon.require("modalities", "image_encode", "unsupported_input", Some(param), "does not accept images"),
1498 MediaKind::Audio => daemon.require("modalities", "gemma_audio_encode", "unsupported_input", Some(param), "does not accept audio"),
1499 }
1500}
1501
1502fn content_param(messages: &[ChatMessage], m: &ChatMessage) -> String {
1503 match messages.iter().position(|x| std::ptr::eq(x, m)) {
1504 Some(i) => format!("messages[{i}].content"),
1505 None => "messages".to_string(),
1506 }
1507}
1508
1509pub(crate) struct SplitContent {
1510 pub(crate) text: String,
1511 pub(crate) images: Vec<(String, Vec<u8>, MediaKind)>,
1512}
1513
1514pub(crate) fn absorbable_system_text(m: &ChatMessage) -> Result<Option<String>, DaemonError> {
1515 if m.role != "system" {
1516 return Ok(None);
1517 }
1518 let split = split_content(&m.content)?;
1519 Ok(split.images.is_empty().then_some(split.text))
1520}
1521
1522pub(crate) fn split_content(v: &Option<serde_json::Value>) -> Result<SplitContent, DaemonError> {
1523 let mut out = SplitContent {
1524 text: String::new(),
1525 images: Vec::new(),
1526 };
1527 match v {
1528 None => {}
1529 Some(serde_json::Value::String(s)) => out.text = s.clone(),
1530 Some(serde_json::Value::Array(parts)) => {
1531 for p in parts {
1532 match p.get("type").and_then(|t| t.as_str()) {
1533 Some("text") => {
1534 if let Some(t) = p.get("text").and_then(|t| t.as_str()) {
1535 out.text.push_str(t);
1536 }
1537 }
1538 Some("image_url") => {
1539 let url = p
1540 .get("image_url")
1541 .and_then(|u| u.get("url"))
1542 .and_then(|u| u.as_str())
1543 .unwrap_or("");
1544 let Some(rest) = url.strip_prefix("data:") else {
1545 return Err(DaemonError::Protocol(
1546 "image_url must be a data: URL (remote images are not fetched)",
1547 ));
1548 };
1549 let Some((_, b64)) = rest.split_once(";base64,") else {
1550 return Err(DaemonError::Protocol("image_url data URL must be base64"));
1551 };
1552 let bytes = base64_decode(b64)?;
1553 out.images
1554 .push((std::mem::take(&mut out.text), bytes, MediaKind::Image));
1555 }
1556 Some("input_audio") => {
1557 let ia = p.get("input_audio");
1558 let fmt = ia
1559 .and_then(|a| a.get("format"))
1560 .and_then(|f| f.as_str())
1561 .unwrap_or("wav");
1562 if fmt != "wav" {
1563 return Err(DaemonError::Protocol(
1564 "input_audio format must be wav (16-bit PCM or float32)",
1565 ));
1566 }
1567 let b64 = ia
1568 .and_then(|a| a.get("data"))
1569 .and_then(|d| d.as_str())
1570 .ok_or(DaemonError::Protocol("input_audio requires base64 data"))?;
1571 let bytes = base64_decode(b64)?;
1572 out.images
1573 .push((std::mem::take(&mut out.text), bytes, MediaKind::Audio));
1574 }
1575 _ => {}
1576 }
1577 }
1578 }
1579 Some(_) => {
1580 return Err(DaemonError::Protocol(
1581 "message content must be a string or parts",
1582 ))
1583 }
1584 }
1585 Ok(out)
1586}
1587
1588#[derive(Deserialize)]
1589pub(crate) struct ReqToolCall {
1590 #[serde(default)]
1591 pub(crate) id: Option<String>,
1592 pub(crate) function: Option<ReqFunction>,
1593}
1594
1595#[derive(Deserialize)]
1596pub(crate) struct ReqFunction {
1597 pub(crate) name: String,
1598 #[serde(default)]
1599 pub(crate) arguments: serde_json::Value,
1600}
1601
1602pub(crate) fn build_session(
1603 daemon: &Daemon,
1604 req: &ChatRequest,
1605 op: &SamplingOverrides,
1606 model: &ModelSamplingDefaults,
1607) -> Result<(u64, bool), DaemonError> {
1608 if let Err(why) = daemon.validate_template_kwargs(&req.template_kwargs()) {
1609 return Err(DaemonError::Constraint(why));
1610 }
1611 let mut params = params_of_for(
1612 req.temperature,
1613 req.top_p,
1614 req.top_k,
1615 req.min_p,
1616 req.seed,
1617 model,
1618 &op.for_daemon(daemon),
1619 );
1620 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
1621
1622 if let Some(c) = req.continues {
1623 if !daemon.renders_per_message() {
1626 return Err(DaemonError::Protocol(
1627 "this model's chat template renders whole conversations; a stored one is not continued by appending",
1628 ));
1629 }
1630 let session = daemon.fork(c.session, c.at_event, Some(params))?;
1631 let last_query = req.messages.iter().rposition(|m| m.role == "user");
1634 for (i, m) in req.messages.iter().enumerate() {
1635 append_chat_message(daemon, session, req, m, last_query.is_none_or(|q| i > q))?;
1636 }
1637 return Ok((session, seed_drawn));
1638 }
1639
1640 let session = daemon.create(None, params)?;
1641
1642 if !daemon.renders_per_message() {
1643 let mut msgs: Vec<crate::codec::ChatMessage> = Vec::with_capacity(req.messages.len());
1644 for m in &req.messages {
1645 let split = split_content(&m.content)?;
1646 let role = match m.role.as_str() {
1647 "system" => role::SYSTEM,
1648 "assistant" => role::ASSISTANT,
1649 "tool" => role::TOOL,
1650 _ => role::USER,
1651 };
1652 if !split.images.is_empty() {
1653 let mut parts: Vec<crate::codec::ContentPart> = Vec::new();
1654 let trailing = split.text;
1655 for (pre, bytes, kind) in split.images {
1656 require_media(daemon, kind, &content_param(&req.messages, m))?;
1657 if !pre.is_empty() {
1658 parts.push(crate::codec::ContentPart::Text(pre));
1659 }
1660 let hash = daemon.put_media(&bytes)?;
1661 parts.push(match kind {
1662 MediaKind::Image => crate::codec::ContentPart::Image { blob: hash },
1663 MediaKind::Audio => crate::codec::ContentPart::Audio { blob: hash },
1664 });
1665 }
1666 if !trailing.is_empty() {
1667 parts.push(crate::codec::ContentPart::Text(trailing));
1668 }
1669 let mut msg = crate::codec::ChatMessage::new(role, String::new());
1670 msg.parts = parts;
1671 msg.reasoning = reasoning_of(m, role);
1672 msgs.push(msg);
1673 continue;
1674 }
1675 let mut msg = crate::codec::ChatMessage::new(role, split.text);
1676 for c in &m.tool_calls {
1677 let Some(f) = &c.function else { continue };
1678 let args = match &f.arguments {
1679 serde_json::Value::String(s) => s.clone(),
1680 other => other.to_string(),
1681 };
1682 msg.tool_calls.push(crate::codec::ToolCallMsg {
1683 id: c.id.clone().unwrap_or_default(),
1684 name: f.name.clone(),
1685 arguments: args,
1686 });
1687 }
1688 msg.reasoning = reasoning_of(m, role);
1689 if role == role::TOOL {
1690 msg.tool_call_id = m.tool_call_id.clone();
1691 msg.name = m.name.clone();
1692 }
1693 msgs.push(msg);
1694 }
1695 let tools: Vec<String> = req.tools.iter().map(|t| t.to_string()).collect();
1696 daemon.append_conversation(session, &msgs, &tools, &req.template_kwargs())?;
1697 return Ok((session, seed_drawn));
1698 }
1699
1700 let (system, tool_jsons) = system_head(req)?;
1701 let rest = if system.is_some() { &req.messages[1..] } else { &req.messages[..] };
1702 daemon.append_system_full(session, system, tool_jsons, &req.template_kwargs())?;
1703 let last_query = req.messages.iter().rposition(|m| m.role == "user");
1705 let skipped = req.messages.len() - rest.len();
1706 for (i, m) in rest.iter().enumerate() {
1707 let after_query = last_query.is_some_and(|q| skipped + i > q);
1708 append_chat_message(daemon, session, req, m, after_query)?;
1709 }
1710 Ok((session, seed_drawn))
1711}
1712
1713pub(crate) fn system_head(req: &ChatRequest) -> Result<(Option<String>, Vec<String>), DaemonError> {
1717 let system = req.messages.first().map(absorbable_system_text).transpose()?.flatten();
1718 let tools = if !req.tools.is_empty() && ToolChoice::parse(&req.tool_choice).declares_tools() {
1719 req.tools.iter().map(|t| t.to_string()).collect()
1720 } else {
1721 Vec::new()
1722 };
1723 Ok((system, tools))
1724}
1725
1726fn append_chat_message(
1727 daemon: &Daemon,
1728 session: u64,
1729 req: &ChatRequest,
1730 m: &ChatMessage,
1731 after_query: bool,
1732) -> Result<(), DaemonError> {
1733 let split = split_content(&m.content)?;
1734 if !split.images.is_empty() {
1735 let r = match m.role.as_str() {
1736 "assistant" => role::ASSISTANT,
1737 "system" => role::SYSTEM,
1738 _ => role::USER,
1739 };
1740 let n = split.images.len();
1741 let mut trailing = split.text;
1742 for (i, (pre, bytes, kind)) in split.images.into_iter().enumerate() {
1743 require_media(daemon, kind, &content_param(&req.messages, m))?;
1744 let hash = daemon.put_media(&bytes)?;
1745 let post = if i + 1 == n {
1746 std::mem::take(&mut trailing)
1747 } else {
1748 String::new()
1749 };
1750 daemon.append_image(session, r, &hash, &pre, &post)?;
1751 }
1752 return Ok(());
1753 }
1754 let content = split.text;
1755 match m.role.as_str() {
1756 "assistant" if !m.tool_calls.is_empty() => {
1757 let calls: Vec<(String, String)> = m
1758 .tool_calls
1759 .iter()
1760 .filter_map(|c| c.function.as_ref())
1761 .map(|f| {
1762 let args = match &f.arguments {
1763 serde_json::Value::String(s) => s.clone(),
1764 v => v.to_string(),
1765 };
1766 (f.name.clone(), args)
1767 })
1768 .collect();
1769 daemon.append_assistant_with_tool_calls(session, content, calls, after_query)?;
1770 }
1771 "assistant" => {
1772 daemon.append_message(session, role::ASSISTANT, content)?;
1773 }
1774 "system" => {
1775 daemon.append_message(session, role::SYSTEM, content)?;
1776 }
1777 "tool" => {
1778 daemon.append_tool_history(session, content)?;
1779 }
1780 _ => {
1781 daemon.append_message(session, role::USER, content)?;
1782 }
1783 }
1784 Ok(())
1785}
1786
1787pub(crate) struct Collected {
1788 pub(crate) content: String,
1789 pub(crate) reasoning: String,
1790 pub(crate) tool_calls: Vec<(u64, String, String)>,
1791 pub(crate) unparsed_tool: String,
1792 pub(crate) broken_calls: Vec<(u64, String, String)>,
1793}
1794
1795pub(crate) fn promote_forced_tool_call(
1796 daemon: &Daemon,
1797 c: &mut Collected,
1798 session: u64,
1799 schemas: Option<&crate::codec::ToolSchemas>,
1800) {
1801 if !c.tool_calls.is_empty() {
1802 return;
1803 }
1804 let raw = c.content.trim().to_string();
1805 if raw.is_empty() {
1806 return;
1807 }
1808 if let Some((name, args)) = daemon.parse_tool_call_with(&raw, schemas) {
1809 c.tool_calls.push((session, name, args));
1810 c.content.clear();
1811 }
1812}
1813
1814pub(crate) fn claim_named_calls(daemon: &Daemon, c: &mut Collected, events: &[crate::CommittedEvent], choice: &ToolChoice) {
1815 if *choice == ToolChoice::None {
1816 return;
1817 }
1818 let mut left = String::new();
1819 for e in events {
1820 if let EventBody::ToolParseFailure { raw } = &e.body {
1821 match daemon.named_tool_call(raw) {
1822 Some((name, args)) => c.broken_calls.push((e.event_id, name, args)),
1823 None => left.push_str(raw),
1824 }
1825 }
1826 }
1827 c.unparsed_tool = left;
1828}
1829
1830pub(crate) fn collect(events: &[crate::CommittedEvent]) -> Collected {
1831 let mut c = Collected {
1832 content: String::new(),
1833 reasoning: String::new(),
1834 tool_calls: Vec::new(),
1835 unparsed_tool: String::new(),
1836 broken_calls: Vec::new(),
1837 };
1838 for e in events {
1839 match &e.body {
1840 EventBody::Generated {
1841 text, channel: ch, ..
1842 } => match *ch {
1843 channel::REASONING => c.reasoning.push_str(text),
1844 channel::TOOL_CALL => {}
1845 _ => c.content.push_str(text),
1846 },
1847 EventBody::ToolUse { name, arguments } => {
1848 c.tool_calls
1849 .push((e.event_id, name.clone(), arguments.clone()));
1850 }
1851 EventBody::ToolParseFailure { raw } => c.unparsed_tool.push_str(raw),
1852 _ => {}
1853 }
1854 }
1855 c
1856}
1857
1858pub(crate) fn finish_reason(
1859 fin: u32,
1860 produced: u32,
1861 max_tokens: u32,
1862 had_tool_calls: bool,
1863) -> &'static str {
1864 let cut = match fin {
1865 finish::LENGTH => true,
1866 finish::EOS | finish::CANCELLED => false,
1867 _ => produced >= max_tokens,
1868 };
1869 if cut {
1870 "length"
1871 } else if had_tool_calls {
1872 "tool_calls"
1873 } else {
1874 "stop"
1875 }
1876}
1877
1878pub(crate) fn prompt_len(daemon: &Daemon, session: u64) -> u32 {
1879 daemon
1880 .store()
1881 .lock()
1882 .expect("store")
1883 .session(session)
1884 .map(|s| s.tokens.len().min(u32::MAX as usize) as u32)
1885 .unwrap_or(0)
1886}
1887
1888pub(crate) fn next_id() -> String {
1889 use std::sync::atomic::{AtomicU64, Ordering};
1890 static N: AtomicU64 = AtomicU64::new(1);
1891 format!("chatcmpl-superfluid-{}", N.fetch_add(1, Ordering::Relaxed))
1892}
1893
1894pub(crate) fn unix_now() -> u64 {
1895 std::time::SystemTime::now()
1896 .duration_since(std::time::UNIX_EPOCH)
1897 .map(|d| d.as_secs())
1898 .unwrap_or(0)
1899}
1900
1901#[cfg(test)]
1902mod access_log_rate_tests {
1903 use super::*;
1904 use std::time::Duration;
1905
1906 #[test]
1907 fn prefill_counts_only_what_was_prefilled() {
1908 let cases = [
1909 (12961u64, 0u64, 10444u64, "1241"),
1910 (14481, 12800, 5716, "294"),
1911 (15627, 14900, 5057, "144"),
1912 (16045, 15900, 2138, "68"),
1913 ];
1914 for (prompt, cached, ttft_ms, want) in cases {
1915 let got = prefill_rate(prompt, cached, Some(Duration::from_millis(ttft_ms)));
1916 assert_eq!(got, want, "prompt={prompt} cached={cached}");
1917 }
1918 }
1919
1920 #[test]
1921 fn a_fully_warm_prompt_prefilled_nothing() {
1922 assert_eq!(
1923 prefill_rate(4096, 4096, Some(Duration::from_millis(80))),
1924 "-"
1925 );
1926 assert_eq!(
1927 prefill_rate(4096, 9000, Some(Duration::from_millis(80))),
1928 "-"
1929 );
1930 }
1931
1932 #[test]
1933 fn a_fim_cache_hit_logs_its_prompt_as_cached() {
1934 let c = |cached| crate::Completion {
1935 text: String::new(),
1936 tokens: vec![1; 16],
1937 cached,
1938 expired: false,
1939 finish: superfluid_abi::finish::NONE,
1940 prompt_tokens: 21,
1941 };
1942 assert_eq!(fim_log_cached(&c(true)), 21);
1943 assert_eq!(
1944 prefill_rate(
1945 21,
1946 fim_log_cached(&c(true)),
1947 Some(Duration::from_micros(60))
1948 ),
1949 "-"
1950 );
1951 assert_eq!(fim_log_cached(&c(false)), 0);
1952 }
1953
1954 #[test]
1955 fn prefill_needs_a_ttft() {
1956 assert_eq!(prefill_rate(1024, 0, None), "-");
1957 assert_eq!(prefill_rate(1024, 0, Some(Duration::ZERO)), "-");
1958 }
1959
1960 fn run(d: &mut DecodeSpan, base: std::time::Instant, stamps: &[(u64, u64)]) {
1961 for (ms, produced) in stamps {
1962 d.stamp_at(base + Duration::from_millis(*ms), *produced);
1963 }
1964 d.finish();
1965 }
1966
1967 #[test]
1968 fn decode_is_the_inter_token_rate() {
1969 let base = std::time::Instant::now();
1970 let mut d = DecodeSpan::default();
1971 run(&mut d, base, &[(0, 1), (781, 200)]);
1972 let (tokens, span) = d.measured();
1973 assert_eq!(tokens, 199);
1974 assert_eq!(decode_rate(tokens, span), "254.8");
1975 }
1976
1977 #[test]
1978 fn decode_excludes_the_whole_first_commit() {
1979 let base = std::time::Instant::now();
1980 let mut d = DecodeSpan::default();
1981 run(&mut d, base, &[(0, 5), (1000, 100)]);
1982 assert_eq!(d.measured().0, 95, "the first commit's 5 predate the span");
1983 assert_eq!(decode_rate(d.measured().0, d.measured().1), "95.0");
1984 }
1985
1986 #[test]
1987 fn decode_spans_do_not_swallow_the_gaps_between_choices() {
1988 let base = std::time::Instant::now();
1989 let mut d = DecodeSpan::default();
1990 run(&mut d, base, &[(0, 1), (500, 51)]);
1991 run(&mut d, base, &[(4500, 1), (5000, 51)]);
1992 let (tokens, span) = d.measured();
1993 assert_eq!(tokens, 100);
1994 assert_eq!(span, Some(Duration::from_millis(1000)), "gap excluded");
1995 assert_eq!(decode_rate(tokens, span), "100.0");
1996 }
1997
1998 #[test]
1999 fn decode_counts_only_what_was_delivered() {
2000 let base = std::time::Instant::now();
2001 let mut d = DecodeSpan::default();
2002 run(&mut d, base, &[(0, 1), (500, 50)]);
2003 let (tokens, span) = d.measured();
2004 assert_eq!(tokens, 49, "the undelivered tail is usage's business");
2005 assert_eq!(decode_rate(tokens, span), "98.0");
2006 }
2007
2008 #[test]
2009 fn decode_declines_to_guess() {
2010 let base = std::time::Instant::now();
2011 let mut d = DecodeSpan::default();
2012 d.finish();
2013 assert_eq!(d.measured(), (0, None));
2014 assert_eq!(decode_rate(0, None), "-");
2015
2016 let mut one = DecodeSpan::default();
2017 run(&mut one, base, &[(0, 40)]);
2018 assert_eq!(decode_rate(one.measured().0, one.measured().1), "-");
2019
2020 assert_eq!(decode_rate(200, Some(Duration::from_millis(150))), "-");
2021 assert_eq!(decode_rate(100, None), "-");
2022 }
2023}
2024
2025#[cfg(test)]
2026mod key_order_tests {
2027 use super::ChatRequest;
2028
2029 #[test]
2030 fn a_tool_keeps_the_key_order_it_was_sent_in() {
2031 let req: ChatRequest = serde_json::from_str(
2032 r#"{"messages":[],"tools":[{"type":"function","function":{"name":"replace_text",
2033 "parameters":{"type":"object","properties":{"path":{"type":"string"},
2034 "old_text":{"type":"string"},"new_text":{"type":"string"}}}}}]}"#,
2035 )
2036 .expect("request");
2037 let t = req.tools[0].to_string();
2038 let at = |k: &str| t.find(k).unwrap_or_else(|| panic!("{k} missing: {t}"));
2039 assert!(at("\"type\"") < at("\"function\""), "{t}");
2040 assert!(
2041 at("\"path\"") < at("\"old_text\"") && at("\"old_text\"") < at("\"new_text\""),
2042 "{t}"
2043 );
2044 }
2045}
2046
2047#[cfg(test)]
2048mod system_absorption_tests {
2049 use super::*;
2050
2051 #[test]
2052 fn assistant_reasoning_is_carried_and_other_roles_are_not() {
2053 let mut m = msg("assistant", serde_json::Value::Null);
2054 m.reasoning_content = Some(" plan ".into());
2055 assert_eq!(reasoning_of(&m, role::ASSISTANT), Some("plan".into()));
2056 m.reasoning_content = None;
2057 m.reasoning = Some("alt".into());
2058 assert_eq!(
2059 reasoning_of(&m, role::ASSISTANT),
2060 Some("alt".into()),
2061 "the other spelling"
2062 );
2063 m.reasoning = Some(" ".into());
2064 assert_eq!(
2065 reasoning_of(&m, role::ASSISTANT),
2066 None,
2067 "blank reasoning is none"
2068 );
2069 let mut u = msg("user", serde_json::json!("hi"));
2070 u.reasoning_content = Some("x".into());
2071 assert_eq!(
2072 reasoning_of(&u, role::USER),
2073 None,
2074 "only assistant turns carry reasoning"
2075 );
2076 }
2077
2078 fn msg(role: &str, content: serde_json::Value) -> ChatMessage {
2079 ChatMessage {
2080 role: role.to_string(),
2081 content: Some(content),
2082 tool_calls: Vec::new(),
2083 tool_call_id: None,
2084 name: None,
2085 reasoning_content: None,
2086 reasoning: None,
2087 }
2088 }
2089
2090 fn image_part() -> serde_json::Value {
2091 serde_json::json!({
2092 "type": "image_url",
2093 "image_url": {"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="}
2094 })
2095 }
2096
2097 #[test]
2098 fn text_only_system_message_is_absorbed() {
2099 let m = msg("system", serde_json::json!("you are helpful"));
2100 assert_eq!(
2101 absorbable_system_text(&m).expect("split"),
2102 Some("you are helpful".to_string()),
2103 "a text-only system message folds into the scaffold"
2104 );
2105 }
2106
2107 #[test]
2108 fn multimodal_system_message_is_left_for_the_media_loop() {
2109 let m = msg(
2110 "system",
2111 serde_json::json!([{"type": "text", "text": "look at this"}, image_part()]),
2112 );
2113 let split = split_content(&m.content).expect("split");
2114 assert_eq!(split.images.len(), 1, "fixture must carry one image part");
2115
2116 assert_eq!(
2117 absorbable_system_text(&m).expect("split"),
2118 None,
2119 "a system message with media must NOT be absorbed — its images \
2120 would be dropped on the floor"
2121 );
2122 }
2123
2124 #[test]
2125 fn audio_system_message_is_left_for_the_media_loop() {
2126 let m = msg(
2127 "system",
2128 serde_json::json!([
2129 {"type": "text", "text": "listen"},
2130 {"type": "input_audio", "input_audio": {"data": "UklGRiQAAABXQVZF", "format": "wav"}}
2131 ]),
2132 );
2133 let split = split_content(&m.content).expect("split");
2134 assert_eq!(split.images.len(), 1, "fixture must carry one audio part");
2135 assert_eq!(absorbable_system_text(&m).expect("split"), None);
2136 }
2137
2138 #[test]
2139 fn non_system_roles_are_never_absorbed() {
2140 for role in ["user", "assistant", "tool", "developer"] {
2141 let m = msg(role, serde_json::json!("hello"));
2142 assert_eq!(
2143 absorbable_system_text(&m).expect("split"),
2144 None,
2145 "{role} must not be absorbed into the system scaffold"
2146 );
2147 }
2148 }
2149}
2150
2151pub(crate) enum RunError {
2152 Daemon(DaemonError),
2153 Worker(String),
2154}
2155impl From<DaemonError> for RunError {
2156 fn from(error: DaemonError) -> Self {
2157 Self::Daemon(error)
2158 }
2159}
2160
2161pub(crate) async fn completions(
2162 daemon: Arc<Daemon>,
2163 model_name: String,
2164 req: CompletionRequest,
2165 policy: RequestPolicy,
2166 hold: Option<crate::keypolicy::ClientKey>,
2167 hooks: crate::keepalive::Hooks,
2168) -> Result<Generation, RunError> {
2169 let qos = policy.qos;
2170 let max_tokens = policy
2171 .limits
2172 .resolve(req.max_tokens, || daemon.tokenize(&req.prompt).len() as u32);
2173 let id = format!(
2174 "cmpl-superfluid-{}",
2175 next_id().rsplit('-').next().unwrap_or("0")
2176 );
2177 let created = unix_now();
2178 let model_defaults = {
2179 let d = daemon.clone();
2180 tokio::task::spawn_blocking(move || {
2181 crate::with_sync_class(Some(qos.class), || d.model_sampling_defaults())
2182 })
2183 .await
2184 .unwrap_or_default()
2185 };
2186 let mut params = params_of_for(
2187 req.temperature,
2188 req.top_p,
2189 req.top_k,
2190 req.min_p,
2191 req.seed,
2192 &model_defaults,
2193 &policy.sampling.for_daemon(&daemon),
2194 );
2195 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
2196 let mut extras = extras_of(
2197 req.presence_penalty,
2198 req.frequency_penalty,
2199 req.repeat_penalty,
2200 &policy.sampling,
2201 &model_defaults,
2202 );
2203 extras.seed_drawn = seed_drawn;
2204 let want_logprobs = req.logprobs.is_some();
2205 extras.want_logprobs = want_logprobs;
2206 extras.top_logprobs = req.logprobs.unwrap_or(0).min(20) as u8;
2207 extras.ignore_eos = req.ignore_eos;
2208 let echo_offset = if req.echo { req.prompt.len() } else { 0 };
2209
2210 if req.stream {
2211 let body = completion_stream(daemon, req, max_tokens, params, extras, qos, hold).await?;
2212 return Ok(Generation {
2213 kind: Kind::Completion,
2214 id,
2215 model: model_name,
2216 created,
2217 warm_header: None,
2218 body,
2219 });
2220 }
2221
2222 let daemon_lp = Arc::clone(&daemon);
2223 let model_log = model_name.clone();
2224 let out = blocking(qos, hold, move || {
2225 let mut hooks = hooks;
2226 let t0 = std::time::Instant::now();
2227 let mut decode = DecodeSpan::default();
2228 let mut produced = 0u64;
2229 let session = daemon.create(None, params)?;
2230 qos.apply(&daemon, session)?;
2231 daemon.append(session, Some(req.prompt.clone()), Vec::new())?;
2232 if hooks.is_on() {
2233 daemon.session_fits(session, None)?;
2234 }
2235 hooks.admitted(&daemon, session);
2236 hooks.check()?;
2237 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
2238 hooks.cancel_if_gone();
2239 if let EventBody::Generated { span, .. } = &ev.body {
2240 if !span.is_empty() {
2241 produced += span.len() as u64;
2242 decode.stamp(produced);
2243 }
2244 }
2245 Ok(())
2246 })?;
2247 let first_tok = decode.first_token();
2248 decode.finish();
2249 let c = collect(&out.events);
2250 let usage = usage(&daemon, session, out.tokens_generated, out.warm_prefix);
2251 access_log(
2252 "POST /v1/completions",
2253 &model_log,
2254 usage.prompt.unwrap_or(0),
2255 out.warm_prefix,
2256 out.tokens_generated as u64,
2257 GenTiming {
2258 t0,
2259 first: first_tok,
2260 decode,
2261 },
2262 finish_reason(out.finish, out.tokens_generated, max_tokens, false),
2263 );
2264 let mut text = String::new();
2265 if req.echo {
2266 text.push_str(&req.prompt);
2267 }
2268 text.push_str(&c.content);
2269 text.push_str(&c.reasoning);
2270 text.push_str(&c.unparsed_tool);
2271 Ok::<_, DaemonError>((out, text, usage))
2272 })
2273 .await;
2274 let (out, text, usage) = out?;
2275 let logprobs = want_logprobs.then(|| decode_logprobs(&daemon_lp, &out.logprobs, echo_offset));
2276 let mut frame = Frame::text(
2277 text,
2278 logprobs,
2279 Some(finish_reason(
2280 out.finish,
2281 out.tokens_generated,
2282 max_tokens,
2283 false,
2284 )),
2285 );
2286 frame.usage = Some(usage);
2287 Ok(Generation {
2288 kind: Kind::Completion,
2289 id,
2290 model: model_name,
2291 created,
2292 warm_header: None,
2293 body: Body::Complete(frame),
2294 })
2295}
2296
2297#[allow(clippy::too_many_arguments)]
2298async fn completion_stream(
2299 daemon: Arc<Daemon>,
2300 req: CompletionRequest,
2301 max_tokens: u32,
2302 params: GenParams,
2303 extras: crate::scheduler::GenExtras,
2304 qos: RequestQos,
2305 hold: Option<crate::keypolicy::ClientKey>,
2306) -> Result<Body, RunError> {
2307 let want_logprobs = req.logprobs.is_some();
2308 let admitted_daemon = Arc::clone(&daemon);
2309 let (req, session) = blocking(qos, hold.clone(), move || {
2310 let session = admitted_daemon.create(None, params)?;
2311 qos.apply(&admitted_daemon, session)?;
2312 admitted_daemon.append(session, Some(req.prompt.clone()), Vec::new())?;
2313 admitted_daemon.session_fits(session, None)?;
2314 Ok((req, session))
2315 })
2316 .await?;
2317 let rx = spawn_stream(qos, hold, move |tx| {
2318 let _closed = crate::keepalive::cancel_when_closed(&daemon, session, tx);
2319 let send = |v: Frame| {
2320 tx.blocking_send(Ok(v))
2321 .map_err(|_| DaemonError::Protocol("generation consumer gone"))
2322 };
2323 let lp_q: std::rc::Rc<
2324 std::cell::RefCell<std::collections::VecDeque<crate::scheduler::TokenLogprob>>,
2325 > = std::rc::Rc::new(std::cell::RefCell::new(std::collections::VecDeque::new()));
2326 let lp_fill = std::rc::Rc::clone(&lp_q);
2327 let mut offset: usize = 0;
2328 let run = || -> Result<(), DaemonError> {
2329 if req.echo {
2330 offset = req.prompt.len();
2331 send(Frame::text(&req.prompt, None, None))?;
2332 }
2333 let out = daemon.generate_streaming_ex_lp(
2334 session,
2335 max_tokens,
2336 extras,
2337 |ev| {
2338 if let EventBody::Generated {
2339 text,
2340 channel: ch,
2341 span,
2342 ..
2343 } = &ev.body
2344 {
2345 let these: Vec<crate::scheduler::TokenLogprob> = if want_logprobs {
2346 let mut q = lp_q.borrow_mut();
2347 (0..span.len()).filter_map(|_| q.pop_front()).collect()
2348 } else {
2349 Vec::new()
2350 };
2351 if !text.is_empty() && *ch != channel::TOOL_CALL {
2352 let lp = if want_logprobs {
2353 let v = Some(decode_logprobs(&daemon, &these, offset));
2354 offset += text.len();
2355 v
2356 } else {
2357 None
2358 };
2359 return send(Frame::text(text, lp, None));
2360 }
2361 }
2362 Ok(())
2363 },
2364 |items| {
2365 lp_fill.borrow_mut().extend(items.iter().cloned());
2366 },
2367 )?;
2368 let fin = finish_reason(out.finish, out.tokens_generated, max_tokens, false);
2369 let mut terminal = Frame::text("", None, Some(fin));
2370 terminal.usage = Some(usage(
2371 &daemon,
2372 session,
2373 out.tokens_generated,
2374 out.warm_prefix,
2375 ));
2376 send(terminal)
2377 };
2378 run()
2379 });
2380 Ok(Body::Stream(rx))
2381}
2382pub(crate) async fn chat(
2383 daemon: Arc<Daemon>,
2384 model_name: String,
2385 req: ChatRequest,
2386 policy: RequestPolicy,
2387 hold: Option<crate::keypolicy::ClientKey>,
2388 hooks: crate::keepalive::Hooks,
2389) -> Result<Generation, RunError> {
2390 let qos = policy.qos;
2391 let explicit_max = req.max_completion_tokens.or(req.max_tokens);
2392 let id = next_id();
2393 let created = unix_now();
2394
2395 if req.stream {
2396 let body = chat_stream(daemon, model_name.clone(), req, policy, explicit_max, hold).await?;
2397 return Ok(Generation {
2398 kind: Kind::Chat,
2399 id,
2400 model: model_name,
2401 created,
2402 warm_header: None,
2403 body,
2404 });
2405 }
2406
2407 let n = req.n.unwrap_or(1).max(1);
2408 let route = req.route();
2409 let stops = req.stop.list();
2410 let model_log = model_name.clone();
2411 let out = blocking(qos, hold, move || {
2412 let mut hooks = hooks;
2413 let t0 = std::time::Instant::now();
2414 let mut decode = DecodeSpan::default();
2415 let mut ttft_first: Option<std::time::Instant> = None;
2416 let mut last_finish = "-";
2417 let model_defaults = daemon.model_sampling_defaults();
2418 let mut extras = extras_of(
2419 req.presence_penalty,
2420 req.frequency_penalty,
2421 req.repeat_penalty,
2422 &policy.sampling,
2423 &model_defaults,
2424 );
2425 let choice = ToolChoice::parse(&req.tool_choice);
2426 if choice.forces_call() {
2427 extras.open_channel = crate::wal::channel::TOOL_CALL;
2428 }
2429 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
2430 extras.tool_schemas = tool_schemas.clone();
2431 let handles = RequestHandles::new(
2432 &daemon,
2433 grammar_handle_for(&daemon, &req, &choice)?,
2434 logit_bias_handle_of(&daemon, &req.logit_bias),
2435 );
2436 handles.apply(&mut extras);
2437 let want_logprobs = req.logprobs.unwrap_or(false);
2438 extras.want_logprobs = want_logprobs;
2439 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
2440 extras.thinking = req.thinking();
2441 extras.ignore_eos = req.ignore_eos;
2442 let cancels = daemon.cancel_registry();
2443 let RequestPolicy {
2444 limits,
2445 sampling: op,
2446 qos,
2447 } = policy;
2448 let mut choices = Vec::with_capacity(n as usize);
2449 let mut completion_tokens = 0u64;
2450 let mut prompt_tokens = 0u64;
2451 let mut warm = 0u64;
2452 let mut warm_first: Option<u64> = None;
2453 let mut spec = (0u64, 0u64);
2454 let mut last_session = None;
2455 for i in 0..n {
2456 if i > 0 && hooks.gone() {
2457 break;
2458 }
2459 let (session, seed_drawn) = build_session(&daemon, &req, &op, &model_defaults)?;
2460 last_session = Some(session);
2461 let mut extras = extras.clone();
2462 extras.seed_drawn = seed_drawn;
2463 qos.apply(&daemon, session)?;
2464 if hooks.is_on() {
2465 daemon.session_fits(session, req.thinking())?;
2466 }
2467 hooks.admitted(&daemon, session);
2468 hooks.check()?;
2469 let max_tokens = limits.resolve(explicit_max, || prompt_len(&daemon, session));
2470 let mut acc = String::new();
2471 let mut produced = 0u64;
2472 let stops = stops.clone();
2473 let cancels = std::sync::Arc::clone(&cancels);
2474 let mut stopped = false;
2475 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
2476 hooks.cancel_if_gone();
2477 if let EventBody::Generated {
2478 text,
2479 channel: ch,
2480 span,
2481 ..
2482 } = &ev.body
2483 {
2484 if !span.is_empty() {
2485 produced += span.len() as u64;
2486 decode.stamp(produced);
2487 }
2488 if *ch == channel::TEXT && !stops.is_empty() && !stopped {
2489 acc.push_str(text);
2490 if stops.iter().any(|s| acc.contains(s.as_str())) {
2491 cancels.cancel(session);
2492 stopped = true;
2493 }
2494 }
2495 }
2496 Ok(())
2497 })?;
2498 ttft_first = ttft_first.or_else(|| decode.first_token());
2499 decode.finish();
2500 let mut c = collect(&out.events);
2501 claim_named_calls(&daemon, &mut c, &out.events, &choice);
2502 if choice.forces_call() {
2503 promote_forced_tool_call(&daemon, &mut c, session, tool_schemas.as_deref());
2504 }
2505 let (content, hit_stop) = truncate_at_stop(&c.content, &stops);
2506 c.content = content;
2507 let usage = usage(&daemon, session, out.tokens_generated, out.warm_prefix);
2508 completion_tokens += usage.completion;
2509 spec.0 += out.spec.0;
2510 spec.1 += out.spec.1;
2511 prompt_tokens = usage.prompt.unwrap_or(prompt_tokens);
2512 warm = warm.max(out.warm_prefix);
2513 warm_first.get_or_insert(out.warm_prefix);
2514 let finish = if !c.broken_calls.is_empty() {
2515 "length"
2516 } else if hit_stop {
2517 "stop"
2518 } else {
2519 finish_reason(
2520 out.finish,
2521 out.tokens_generated,
2522 max_tokens,
2523 !c.tool_calls.is_empty() && choice != ToolChoice::None,
2524 )
2525 };
2526 choices.push(Choice {
2527 index: i,
2528 message: message(&c, &choice),
2529 finish: Some(finish),
2530 logprobs: want_logprobs.then(|| decode_logprobs(&daemon, &out.logprobs, 0)),
2531 ..Choice::default()
2532 });
2533 last_finish = finish;
2534 }
2535 access_log(
2536 route,
2537 &model_log,
2538 prompt_tokens,
2539 warm_first.unwrap_or(warm),
2540 completion_tokens,
2541 GenTiming {
2542 t0,
2543 first: ttft_first,
2544 decode,
2545 },
2546 last_finish,
2547 );
2548 Ok::<_, DaemonError>((choices, prompt_tokens, completion_tokens, warm, spec, last_session))
2549 })
2550 .await;
2551 let (choices, prompt_tokens, completion_tokens, warm, spec, session) = out?;
2552 let frame = Frame {
2553 choices,
2554 usage: Some(Usage::complete(
2555 prompt_tokens,
2556 completion_tokens,
2557 Some(warm.min(prompt_tokens)),
2558 )),
2559 spec: (spec.0 > 0).then_some(spec),
2560 session,
2561 ..Frame::default()
2562 };
2563 Ok(Generation {
2564 kind: Kind::Chat,
2565 id,
2566 model: model_name,
2567 created,
2568 warm_header: Some(warm),
2569 body: Body::Complete(frame),
2570 })
2571}
2572
2573async fn chat_stream(
2574 daemon: Arc<Daemon>,
2575 model_name: String,
2576 req: ChatRequest,
2577 policy: RequestPolicy,
2578 explicit_max: Option<u32>,
2579 hold: Option<crate::keypolicy::ClientKey>,
2580) -> Result<Body, RunError> {
2581 let RequestPolicy {
2582 limits,
2583 sampling: op,
2584 qos,
2585 } = policy;
2586 let t0 = std::time::Instant::now();
2587 let admitted_daemon = Arc::clone(&daemon);
2588 let (req, session, seed_drawn, model_defaults) = blocking(qos, hold.clone(), move || {
2589 let model_defaults = admitted_daemon.model_sampling_defaults();
2590 let (session, seed_drawn) = build_session(&admitted_daemon, &req, &op, &model_defaults)?;
2591 qos.apply(&admitted_daemon, session)?;
2592 admitted_daemon.session_fits(session, req.thinking())?;
2593 Ok((req, session, seed_drawn, model_defaults))
2594 })
2595 .await?;
2596 let rx = spawn_stream(qos, hold, move |tx| {
2597 let _closed = crate::keepalive::cancel_when_closed(&daemon, session, tx);
2598 let send = |v: Frame| -> Result<(), DaemonError> {
2599 tx.blocking_send(Ok(v))
2600 .map_err(|_| DaemonError::Protocol("generation consumer gone"))
2601 };
2602 let run = || -> Result<(), DaemonError> {
2603 send(Frame::chat(
2604 Message {
2605 assistant: true,
2606 ..Message::default()
2607 },
2608 None,
2609 ))?;
2610 let mut tool_index = 0usize;
2611 let mut orphaned_calls = 0usize;
2612 let mut extras = extras_of(
2613 req.presence_penalty,
2614 req.frequency_penalty,
2615 req.repeat_penalty,
2616 &op,
2617 &model_defaults,
2618 );
2619 extras.seed_drawn = seed_drawn;
2620 let choice = ToolChoice::parse(&req.tool_choice);
2621 if choice.forces_call() {
2622 extras.open_channel = crate::wal::channel::TOOL_CALL;
2623 }
2624 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
2625 extras.tool_schemas = tool_schemas.clone();
2626 let mut frag =
2627 crate::tool_fragment::ToolCallStream::new(if choice == ToolChoice::None {
2628 crate::tool_fragment::ToolFragmentMode::None
2629 } else {
2630 daemon.tool_fragment_mode()
2631 })
2632 .with_schemas(tool_schemas.clone());
2633 let mut open_call_id = String::new();
2634 let handles = RequestHandles::new(
2635 &daemon,
2636 grammar_handle_for(&daemon, &req, &choice)?,
2637 logit_bias_handle_of(&daemon, &req.logit_bias),
2638 );
2639 handles.apply(&mut extras);
2640 let want_logprobs = req.logprobs.unwrap_or(false);
2641 extras.want_logprobs = want_logprobs;
2642 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
2643 extras.thinking = req.thinking();
2644 extras.ignore_eos = req.ignore_eos;
2645 let lp_q: std::rc::Rc<
2646 std::cell::RefCell<std::collections::VecDeque<crate::scheduler::TokenLogprob>>,
2647 > = std::rc::Rc::new(std::cell::RefCell::new(std::collections::VecDeque::new()));
2648 let lp_fill = std::rc::Rc::clone(&lp_q);
2649 let max_tokens = limits.resolve(explicit_max, || prompt_len(&daemon, session));
2650 let stops = req.stop.list();
2651 let cancels = daemon.cancel_registry();
2652 let delta_mode = !want_logprobs;
2653 let continuous_usage = req
2654 .stream_options
2655 .as_ref()
2656 .is_some_and(|o| o.continuous_usage_stats);
2657 struct StreamText {
2658 usage_now: Option<u32>,
2659 committed: u32,
2660 decode: DecodeSpan,
2661 content_acc: String,
2662 content_sent: usize,
2663 stopped: bool,
2664 dead: bool,
2665 }
2666 let st = std::rc::Rc::new(std::cell::RefCell::new(StreamText {
2667 usage_now: None,
2668 committed: 0,
2669 decode: DecodeSpan::default(),
2670 content_acc: String::new(),
2671 content_sent: 0,
2672 stopped: false,
2673 dead: false,
2674 }));
2675 let push_text =
2676 |s: &mut StreamText, text: &str, lp: Option<Logprobs>| -> Result<(), DaemonError> {
2677 if s.stopped {
2678 return Ok(());
2679 }
2680 if choice.forces_call() {
2681 s.content_acc.push_str(text);
2682 return Ok(());
2683 }
2684 let start = s.content_sent;
2685 s.content_acc.push_str(text);
2686 let tail = &s.content_acc[start..];
2687 let mut cut: Option<usize> = None;
2688 for stop in &stops {
2689 if let Some(i) = tail.find(stop.as_str()) {
2690 cut = Some(cut.map_or(i, |c| c.min(i)));
2691 }
2692 }
2693 let hit = cut.is_some();
2694 let end = match cut {
2695 Some(i) => start + i,
2696 None => start + tail.len() - stop_prefix_holdback(tail, &stops),
2697 };
2698 let piece = s.content_acc.get(start..end).unwrap_or("").to_string();
2699 s.content_sent = s.content_sent.max(end);
2700 if !piece.is_empty() {
2701 let mut ck = Frame::chat(Message::content(piece), None);
2702 if let Some(lp) = lp {
2703 ck.choices[0].logprobs = Some(lp);
2704 }
2705 if let Some(n) = s.usage_now {
2706 ck.usage = Some(Usage::partial(n));
2707 }
2708 send(ck)?;
2709 }
2710 if hit {
2711 cancels.cancel(session);
2712 s.stopped = true;
2713 }
2714 Ok(())
2715 };
2716 let st_delta = std::rc::Rc::clone(&st);
2717 let on_delta = |ch: u32, text: &str, produced: u32| {
2718 if !delta_mode || text.is_empty() {
2719 return;
2720 }
2721 let mut s = st_delta.borrow_mut();
2722 if continuous_usage {
2723 s.usage_now = Some(produced);
2724 }
2725 if s.dead {
2726 return;
2727 }
2728 s.decode.stamp(produced as u64);
2729 let r = if ch == channel::REASONING {
2730 let mut ck = Frame::chat(Message::reasoning(text), None);
2731 if let Some(n) = s.usage_now {
2732 ck.usage = Some(Usage::partial(n));
2733 }
2734 send(ck)
2735 } else {
2736 push_text(&mut s, text, None)
2737 };
2738 if r.is_err() {
2739 cancels.cancel(session);
2740 s.dead = true;
2741 }
2742 };
2743 let st_ev = std::rc::Rc::clone(&st);
2744 let on_event = |ev: &crate::CommittedEvent| {
2745 match &ev.body {
2746 EventBody::Generated {
2747 text,
2748 channel: ch,
2749 span,
2750 ..
2751 } => {
2752 let mut s = st_ev.borrow_mut();
2753 let covered_by_delta = delta_mode
2754 && (*ch == channel::TEXT || *ch == channel::REASONING)
2755 && !text.is_empty();
2756 s.committed += span.len() as u32;
2757 let committed = s.committed as u64;
2758 if !covered_by_delta && !span.is_empty() {
2759 s.decode.stamp(committed);
2760 }
2761 if continuous_usage && !delta_mode {
2762 s.usage_now = Some(s.committed);
2763 }
2764 let these: Vec<crate::scheduler::TokenLogprob> = if want_logprobs {
2765 let mut q = lp_q.borrow_mut();
2766 (0..span.len()).filter_map(|_| q.pop_front()).collect()
2767 } else {
2768 Vec::new()
2769 };
2770 if *ch == channel::TOOL_CALL {
2771 for d in frag.feed(text) {
2772 if d.opening {
2773 open_call_id = format!("call_{}", ev.event_id);
2774 }
2775 let json = Message::tool(&d, tool_index, &open_call_id);
2776 send(Frame::chat(json, None))?;
2777 }
2778 return Ok(());
2779 }
2780 if text.is_empty() {
2781 return Ok(());
2782 }
2783 if delta_mode {
2784 return Ok(());
2785 }
2786 if *ch == channel::REASONING {
2787 let mut ck = Frame::chat(Message::reasoning(text), None);
2788 if let Some(n) = s.usage_now {
2789 ck.usage = Some(Usage::partial(n));
2790 }
2791 return send(ck);
2792 }
2793 let lp = want_logprobs.then(|| decode_logprobs(&daemon, &these, 0));
2794 push_text(&mut s, text, lp)
2795 }
2796 EventBody::ToolParseFailure { raw } => {
2797 if choice != ToolChoice::None && !frag.opened() {
2798 if let Some((name, arguments)) = daemon.named_tool_call(raw) {
2799 let delta = Message::complete_tool(
2800 tool_index,
2801 format!("call_{}", ev.event_id),
2802 name,
2803 arguments,
2804 );
2805 tool_index += 1;
2806 orphaned_calls += 1;
2807 return send(Frame::chat(delta, None));
2808 }
2809 }
2810 if frag.opened() {
2811 orphaned_calls += 1;
2812 tool_index += 1;
2813 }
2814 frag.reset();
2815 let mut ck = Frame::chat(Message::content(raw), None);
2816 if continuous_usage {
2817 ck.usage = Some(Usage::partial(st_ev.borrow().committed));
2818 }
2819 send(ck)
2820 }
2821 EventBody::ToolUse { name, arguments } => {
2822 if choice == ToolChoice::None {
2823 let raw = format!(
2824 "{{\"name\":{},\"arguments\":{}}}",
2825 serde_json::Value::String(name.clone()),
2826 arguments
2827 );
2828 let mut ck = Frame::chat(Message::content(raw), None);
2829 if continuous_usage {
2830 ck.usage = Some(Usage::partial(st_ev.borrow().committed));
2831 }
2832 return send(ck);
2833 }
2834 if frag.opened() {
2835 for d in frag.finish(arguments) {
2836 send(Frame::chat(
2837 Message::tool(&d, tool_index, &open_call_id),
2838 None,
2839 ))?;
2840 }
2841 frag.reset();
2842 tool_index += 1;
2843 return Ok(());
2844 }
2845 let delta = Message::complete_tool(
2846 tool_index,
2847 format!("call_{}", ev.event_id),
2848 name.clone(),
2849 arguments.clone(),
2850 );
2851 tool_index += 1;
2852 send(Frame::chat(delta, None))
2853 }
2854 _ => Ok(()),
2855 }
2856 };
2857 let on_logprobs = |items: &[crate::scheduler::TokenLogprob]| {
2858 lp_fill.borrow_mut().extend(items.iter().cloned());
2859 };
2860 let out = if delta_mode {
2861 daemon.generate_streaming_delta(
2862 session,
2863 max_tokens,
2864 extras,
2865 on_event,
2866 on_logprobs,
2867 on_delta,
2868 )?
2869 } else {
2870 daemon.generate_streaming_ex_lp(
2871 session,
2872 max_tokens,
2873 extras,
2874 on_event,
2875 on_logprobs,
2876 )?
2877 };
2878 let st = st.borrow();
2879 let (mut decode, stopped) = (st.decode, st.stopped);
2880 let first_tok = decode.first_token();
2881 decode.finish();
2882 let (content_acc, content_sent) = (st.content_acc.clone(), st.content_sent);
2883 drop(st);
2884 let mut c = collect(&out.events);
2885 if !stopped && !choice.forces_call() && content_sent < content_acc.len() {
2886 let rest = content_acc[content_sent..].to_string();
2887 let mut ck = Frame::chat(Message::content(rest), None);
2888 if continuous_usage {
2889 ck.usage = Some(Usage::partial(out.tokens_generated));
2890 }
2891 send(ck)?;
2892 }
2893 if choice.forces_call() {
2894 c.content = content_acc.clone();
2895 promote_forced_tool_call(&daemon, &mut c, session, tool_schemas.as_deref());
2896 if tool_index > 0 {
2897 } else if let Some((id, name, args)) = c.tool_calls.first() {
2898 send(Frame::chat(
2899 Message::complete_tool(
2900 tool_index,
2901 format!("call_{id}"),
2902 name.clone(),
2903 args.clone(),
2904 ),
2905 None,
2906 ))?;
2907 } else if !c.content.is_empty() {
2908 let mut ck = Frame::chat(Message::content(c.content), None);
2909 if continuous_usage {
2910 ck.usage = Some(Usage::partial(out.tokens_generated));
2911 }
2912 send(ck)?;
2913 }
2914 }
2915 let fin = if stopped {
2916 "stop"
2917 } else {
2918 finish_reason(
2919 out.finish,
2920 out.tokens_generated,
2921 max_tokens,
2922 !c.tool_calls.is_empty() && choice != ToolChoice::None,
2923 )
2924 };
2925 let fin = if orphaned_calls > 0 { "length" } else { fin };
2926 let mut terminal = Frame::chat(Message::default(), Some(fin));
2927 terminal.usage = Some(usage(
2928 &daemon,
2929 session,
2930 out.tokens_generated,
2931 out.warm_prefix,
2932 ));
2933 access_log(
2934 &format!("{} (stream)", req.route()),
2935 &model_name,
2936 terminal.usage.as_ref().and_then(|u| u.prompt).unwrap_or(0),
2937 out.warm_prefix,
2938 out.tokens_generated as u64,
2939 GenTiming {
2940 t0,
2941 first: first_tok,
2942 decode,
2943 },
2944 fin,
2945 );
2946 terminal.warm = Some(out.warm_prefix);
2947 terminal.spec = (out.spec.0 > 0).then_some(out.spec);
2948 terminal.session = Some(session);
2949 send(terminal)
2950 };
2951 run()
2952 });
2953
2954 Ok(Body::Stream(rx))
2955}
2956const FIM_DEFAULT_MAX_TOKENS: u32 = 128;
2957
2958#[allow(clippy::too_many_arguments)]
2959pub(crate) async fn fim_completions(
2960 daemon: Arc<Daemon>,
2961 model_name: String,
2962 req: CompletionRequest,
2963 limits: TokenLimits,
2964 sampling: SamplingOverrides,
2965 mode: u8,
2966 key: Option<crate::keypolicy::ClientKey>,
2967) -> Result<Generation, RunError> {
2968 if !daemon.fim_supported() {
2969 return Err(DaemonError::NoFimDialect.into());
2970 }
2971 let hold = key;
2972 let max_tokens = req
2973 .max_tokens
2974 .or(limits.default_max_tokens)
2975 .unwrap_or(FIM_DEFAULT_MAX_TOKENS)
2976 .max(1);
2977 let serving = daemon
2978 .fim_satellite()
2979 .unwrap_or_else(|| Arc::clone(&daemon));
2980 let model_defaults = {
2981 let d = Arc::clone(&serving);
2982 tokio::task::spawn_blocking(move || {
2983 crate::with_sync_class(Some(crate::qos::INLINE_COMPLETION), || {
2984 d.model_sampling_defaults()
2985 })
2986 })
2987 .await
2988 .unwrap_or_default()
2989 };
2990 let op = sampling.for_daemon(&serving);
2991 let mut params = params_of_for(
2992 req.temperature,
2993 req.top_p,
2994 req.top_k,
2995 req.min_p,
2996 req.seed,
2997 &model_defaults,
2998 &op,
2999 );
3000 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
3001 let mut extras = extras_of(
3002 req.presence_penalty,
3003 req.frequency_penalty,
3004 req.repeat_penalty,
3005 &op,
3006 &model_defaults,
3007 );
3008 extras.seed_drawn = seed_drawn;
3009 extras.ignore_eos = req.ignore_eos;
3010 let fim = crate::FimRequest {
3011 prefix: req.prompt,
3012 suffix: req.suffix.unwrap_or_default(),
3013 mode,
3014 max_tokens,
3015 params,
3016 extras,
3017 };
3018 let id = format!(
3019 "cmpl-superfluid-{}",
3020 next_id().rsplit('-').next().unwrap_or("0")
3021 );
3022 let created = unix_now();
3023 let served_by = daemon
3024 .fim_satellite_name()
3025 .unwrap_or_else(|| model_name.clone());
3026 let log_model = served_by.clone();
3027 let fin_of = move |c: &crate::Completion| {
3028 if c.expired {
3029 "length"
3030 } else {
3031 finish_reason(c.finish, c.tokens.len() as u32, max_tokens, false)
3032 }
3033 };
3034 let ext = move |c: &crate::Completion| FimMetadata {
3035 cached: c.cached,
3036 expired: c.expired,
3037 served_by: served_by.clone(),
3038 };
3039 if req.stream {
3040 let admitted_daemon = Arc::clone(&daemon);
3041 let fim = blocking(
3042 RequestQos {
3043 class: crate::qos::INLINE_COMPLETION,
3044 batch_invariant: false,
3045 },
3046 hold.clone(),
3047 move || {
3048 admitted_daemon.fim_fits(&fim)?;
3049 Ok(fim)
3050 },
3051 )
3052 .await?;
3053 let (tx, rx) = tokio::sync::mpsc::channel(64);
3054 tokio::task::spawn_blocking(move || {
3055 let _hold = hold;
3056 let t0 = std::time::Instant::now();
3057 let mut decode = DecodeSpan::default();
3058 let mut delivered = 0usize;
3059 let res = daemon.complete_streaming(&fim, |piece, produced| {
3060 decode.stamp(produced as u64);
3061 delivered = delivered.max(produced);
3062 match tx.blocking_send(Ok(Frame::text(piece, None, None))) {
3063 Ok(()) => std::ops::ControlFlow::Continue(()),
3064 Err(_) => std::ops::ControlFlow::Break(()),
3065 }
3066 });
3067 match res {
3068 Ok(c) => {
3069 let fin = fin_of(&c);
3070 let first = decode.first_token();
3071 decode.finish();
3072 access_log(
3073 "POST /v1/completions (fim)",
3074 &log_model,
3075 c.prompt_tokens as u64,
3076 fim_log_cached(&c),
3077 c.tokens.len().max(delivered) as u64,
3078 GenTiming { t0, first, decode },
3079 fin,
3080 );
3081 let mut last = Frame::text("", None, Some(fin));
3082 last.fim = Some(ext(&c));
3083 last.usage = Some(Usage::complete(
3084 c.prompt_tokens as u64,
3085 c.tokens.len() as u64,
3086 None,
3087 ));
3088 let _ = tx.blocking_send(Ok(last));
3089 }
3090 Err(e) => {
3091 let client_error = matches!(
3092 e,
3093 DaemonError::StreamTooLong { .. } | DaemonError::NoFimDialect
3094 );
3095 let _ = tx.blocking_send(Err(StreamFailure::new(e, client_error)));
3096 }
3097 }
3098 });
3099 return Ok(Generation {
3100 kind: Kind::Completion,
3101 id,
3102 model: model_name,
3103 created,
3104 warm_header: None,
3105 body: Body::Stream(rx),
3106 });
3107 }
3108 let out = tokio::task::spawn_blocking(move || {
3109 let _hold = hold;
3110 let t0 = std::time::Instant::now();
3111 let mut decode = DecodeSpan::default();
3112 let c = daemon.complete_streaming(&fim, |_, produced| {
3113 decode.stamp(produced as u64);
3114 std::ops::ControlFlow::Continue(())
3115 })?;
3116 let first = decode.first_token();
3117 decode.finish();
3118 access_log(
3119 "POST /v1/completions (fim)",
3120 &log_model,
3121 c.prompt_tokens as u64,
3122 fim_log_cached(&c),
3123 c.tokens.len() as u64,
3124 GenTiming { t0, first, decode },
3125 fin_of(&c),
3126 );
3127 Ok::<_, DaemonError>(c)
3128 })
3129 .await;
3130 let c = out.map_err(|e| RunError::Worker(e.to_string()))??;
3131 let mut frame = Frame::text(&c.text, None, Some(fin_of(&c)));
3132 frame.usage = Some(Usage::complete(
3133 c.prompt_tokens as u64,
3134 c.tokens.len() as u64,
3135 None,
3136 ));
3137 frame.fim = Some(ext(&c));
3138 Ok(Generation {
3139 kind: Kind::Completion,
3140 id,
3141 model: model_name,
3142 created,
3143 warm_header: None,
3144 body: Body::Complete(frame),
3145 })
3146}
3147
3148pub(crate) fn chat_once(
3149 daemon: &Daemon,
3150 req: &ChatRequest,
3151 policy: RequestPolicy,
3152) -> Result<Frame, DaemonError> {
3153 let RequestPolicy {
3154 limits,
3155 sampling: op,
3156 qos,
3157 } = policy;
3158 let _class = crate::SyncClassScope::enter(Some(qos.class));
3159 let model_defaults = daemon.model_sampling_defaults();
3160 let mut extras = extras_of(
3161 req.presence_penalty,
3162 req.frequency_penalty,
3163 req.repeat_penalty,
3164 &op,
3165 &model_defaults,
3166 );
3167 let choice = ToolChoice::parse(&req.tool_choice);
3168 if choice.forces_call() {
3169 extras.open_channel = crate::wal::channel::TOOL_CALL;
3170 }
3171 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
3172 extras.tool_schemas = tool_schemas.clone();
3173 let handles = RequestHandles::new(
3174 daemon,
3175 grammar_handle_for(daemon, req, &choice)?,
3176 logit_bias_handle_of(daemon, &req.logit_bias),
3177 );
3178 handles.apply(&mut extras);
3179 let want_logprobs = req.logprobs.unwrap_or(false);
3180 extras.want_logprobs = want_logprobs;
3181 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
3182 extras.thinking = req.thinking();
3183 extras.ignore_eos = req.ignore_eos;
3184 let stops = req.stop.list();
3185 let (session, seed_drawn) = build_session(daemon, req, &op, &model_defaults)?;
3186 extras.seed_drawn = seed_drawn;
3187 qos.apply(daemon, session)?;
3188 let max_tokens = limits.resolve(req.max_completion_tokens.or(req.max_tokens), || {
3189 prompt_len(daemon, session)
3190 });
3191 let cancels = daemon.cancel_registry();
3192 let mut acc = String::new();
3193 let mut stopped = false;
3194 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
3195 if let EventBody::Generated {
3196 text, channel: ch, ..
3197 } = &ev.body
3198 {
3199 if *ch == channel::TEXT && !stops.is_empty() && !stopped {
3200 acc.push_str(text);
3201 if stops.iter().any(|s| acc.contains(s.as_str())) {
3202 cancels.cancel(session);
3203 stopped = true;
3204 }
3205 }
3206 }
3207 Ok(())
3208 });
3209 let out = out?;
3210 let mut c = collect(&out.events);
3211 claim_named_calls(daemon, &mut c, &out.events, &choice);
3212 if choice.forces_call() {
3213 promote_forced_tool_call(daemon, &mut c, session, tool_schemas.as_deref());
3214 }
3215 let (content, hit_stop) = truncate_at_stop(&c.content, &stops);
3216 c.content = content;
3217 let usage = usage(daemon, session, out.tokens_generated, out.warm_prefix);
3218 let finish = if !c.broken_calls.is_empty() {
3219 "length"
3220 } else if hit_stop {
3221 "stop"
3222 } else {
3223 finish_reason(
3224 out.finish,
3225 out.tokens_generated,
3226 max_tokens,
3227 !c.tool_calls.is_empty() && choice != ToolChoice::None,
3228 )
3229 };
3230 Ok(Frame {
3231 choices: vec![Choice {
3232 message: message(&c, &choice),
3233 finish: Some(finish),
3234 logprobs: want_logprobs.then(|| decode_logprobs(daemon, &out.logprobs, 0)),
3235 ..Choice::default()
3236 }],
3237 usage: Some(usage),
3238 spec: (out.spec.0 > 0).then_some(out.spec),
3239 ..Frame::default()
3240 })
3241}
3242
3243pub(crate) async fn blocking<T: Send + 'static>(
3244 qos: RequestQos,
3245 hold: Option<crate::keypolicy::ClientKey>,
3246 work: impl FnOnce() -> Result<T, DaemonError> + Send + 'static,
3247) -> Result<T, RunError> {
3248 tokio::task::spawn_blocking(move || {
3249 let _hold = hold;
3250 let _class = crate::SyncClassScope::enter(Some(qos.class));
3251 work()
3252 })
3253 .await
3254 .map_err(|e| RunError::Worker(e.to_string()))?
3255 .map_err(RunError::Daemon)
3256}
3257
3258pub(crate) fn base64_decode(data: &str) -> Result<Vec<u8>, DaemonError> {
3259 use base64::Engine;
3260 let cleaned: String = data.chars().filter(|c| !c.is_whitespace()).collect();
3261 base64::engine::general_purpose::STANDARD
3262 .decode(&cleaned)
3263 .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(&cleaned))
3264 .map_err(|_| DaemonError::Protocol("invalid base64 image data"))
3265}
3266
3267pub(crate) fn spawn_stream<T: Send + 'static>(
3268 qos: RequestQos,
3269 hold: Option<crate::keypolicy::ClientKey>,
3270 work: impl FnOnce(&tokio::sync::mpsc::Sender<Result<T, StreamFailure>>) -> Result<(), DaemonError>
3271 + Send
3272 + 'static,
3273) -> tokio::sync::mpsc::Receiver<Result<T, StreamFailure>> {
3274 let (tx, rx) = tokio::sync::mpsc::channel(64);
3275 tokio::task::spawn_blocking(move || {
3276 let _hold = hold;
3277 let _class = crate::SyncClassScope::enter(Some(qos.class));
3278 if let Err(error) = work(&tx) {
3279 let client_error = matches!(
3280 error,
3281 DaemonError::Constraint(_)
3282 | DaemonError::TemplateRefused(_)
3283 | DaemonError::Unsupported(_)
3284 | DaemonError::StreamTooLong { .. }
3285 | DaemonError::NoFimDialect
3286 );
3287 let _ = tx.blocking_send(Err(StreamFailure::new(error, client_error)));
3288 }
3289 });
3290 rx
3291}