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 measured_tokens: u64,
1113 measured_elapsed: std::time::Duration,
1114 scheduler_timed: bool,
1117}
1118
1119impl DecodeSpan {
1120 pub(crate) fn stamp(&mut self, produced: u64) {
1121 self.stamp_at(std::time::Instant::now(), produced);
1122 }
1123
1124 fn stamp_at(&mut self, now: std::time::Instant, produced: u64) {
1125 if self.first.is_none() {
1126 self.first = Some(now);
1127 self.first_count = produced;
1128 }
1129 self.last = Some(now);
1130 self.last_count = produced.max(self.last_count);
1131 }
1132
1133 pub(crate) fn first_token(&self) -> Option<std::time::Instant> {
1134 self.first
1135 }
1136
1137 pub(crate) fn finish(&mut self) {
1138 if let (Some(f), Some(l)) = (self.first, self.last) {
1139 self.tokens += self.last_count.saturating_sub(self.first_count);
1140 self.elapsed += l.saturating_duration_since(f);
1141 }
1142 self.first = None;
1143 self.last = None;
1144 self.first_count = 0;
1145 self.last_count = 0;
1146 }
1147
1148 pub(crate) fn add_measured(&mut self, (tokens, secs): (u64, f64)) {
1155 self.scheduler_timed = true;
1156 if tokens > 0 && secs > 0.0 {
1157 self.measured_tokens += tokens;
1158 self.measured_elapsed += std::time::Duration::from_secs_f64(secs);
1159 }
1160 }
1161
1162 fn measured(&self) -> (u64, Option<std::time::Duration>) {
1163 if self.scheduler_timed {
1164 match self.measured_tokens {
1165 0 => (0, None),
1166 n => (n, Some(self.measured_elapsed)),
1167 }
1168 } else if self.elapsed == std::time::Duration::ZERO {
1169 (0, None)
1170 } else {
1171 (self.tokens, Some(self.elapsed))
1172 }
1173 }
1174}
1175
1176#[derive(Clone, Copy)]
1177pub(crate) struct GenTiming {
1178 pub t0: std::time::Instant,
1179 pub first: Option<std::time::Instant>,
1180 pub decode: DecodeSpan,
1181}
1182
1183fn fim_log_cached(c: &crate::Completion) -> u64 {
1184 if c.cached {
1185 c.prompt_tokens as u64
1186 } else {
1187 0
1188 }
1189}
1190
1191fn prefill_rate(prompt: u64, cached: u64, ttft: Option<std::time::Duration>) -> String {
1192 let prefilled = prompt - cached.min(prompt);
1193 ttft.filter(|d| d.as_secs_f64() > 0.0 && prefilled > 0)
1194 .map(|d| format!("{:.0}", prefilled as f64 / d.as_secs_f64()))
1195 .unwrap_or_else(|| "-".into())
1196}
1197
1198fn decode_rate(tokens: u64, span: Option<std::time::Duration>) -> String {
1199 span.filter(|d| tokens > 0 && d.as_millis() as u64 >= tokens)
1200 .map(|d| format!("{:.1}", tokens as f64 / d.as_secs_f64()))
1201 .unwrap_or_else(|| "-".into())
1202}
1203
1204pub(crate) fn access_log(
1205 route: &str,
1206 model: &str,
1207 prompt: u64,
1208 cached: u64,
1209 completion: u64,
1210 timing: GenTiming,
1211 finish: &str,
1212) {
1213 let GenTiming { t0, first, decode } = timing;
1214 let total_ms = t0.elapsed().as_millis();
1215 let ttft = first.map(|f| f.saturating_duration_since(t0));
1216 let (decode_tokens, decode_span) = decode.measured();
1217 let prefill = prefill_rate(prompt, cached, ttft);
1218 let decode = decode_rate(decode_tokens, decode_span);
1219 tracing::info!(
1220 "<-- 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}",
1221 ttft.map(|d| d.as_millis()).unwrap_or(0),
1222 );
1223}
1224
1225pub(crate) fn extras_of(
1226 presence: Option<f32>,
1227 frequency: Option<f32>,
1228 repeat: Option<f32>,
1229 op: &SamplingOverrides,
1230 model: &ModelSamplingDefaults,
1231) -> crate::scheduler::GenExtras {
1232 let mut frequency_penalty = frequency.unwrap_or(0.0);
1233 let mut repeat_penalty = repeat_penalty_for(repeat, op, model);
1234 if op.fallback.is_some_and(|f| f.frequency_penalty_as_repeat)
1235 && repeat.is_none()
1236 && op.repeat_penalty.is_none()
1237 {
1238 if presence.is_some() {
1239 repeat_penalty = 1.0;
1240 } else if let Some(f) = frequency {
1241 repeat_penalty = 1.0 + f;
1242 frequency_penalty = 0.0;
1243 }
1244 }
1245 crate::scheduler::GenExtras {
1246 ephemeral: true,
1247 keep_prefix: false,
1248 presence_penalty: presence.unwrap_or(0.0),
1249 frequency_penalty,
1250 repeat_penalty,
1251 grammar_handle: 0,
1252 logit_bias_handle: 0,
1253 want_logprobs: false,
1254 top_logprobs: 0,
1255 wants_deltas: false,
1256 open_channel: 0,
1257 thinking: None,
1258 ignore_eos: false,
1259 seed_drawn: false,
1260 tool_schemas: None,
1261 }
1262}
1263
1264pub(crate) fn logit_bias_handle_of(
1265 daemon: &Daemon,
1266 map: &Option<std::collections::HashMap<String, f32>>,
1267) -> Option<u32> {
1268 let map = map.as_ref()?;
1269 if map.is_empty() {
1270 return None;
1271 }
1272 let mut tokens = Vec::with_capacity(map.len());
1273 let mut values = Vec::with_capacity(map.len());
1274 for (k, v) in map {
1275 if let Ok(t) = k.parse::<i32>() {
1276 tokens.push(t);
1277 values.push(*v);
1278 }
1279 }
1280 if tokens.is_empty() {
1281 return None;
1282 }
1283 daemon.logit_bias_create(&tokens, &values).ok()
1284}
1285
1286#[derive(Debug, Default, Deserialize)]
1287pub(crate) struct StreamOptions {
1288 #[serde(default)]
1289 #[allow(dead_code)]
1290 pub(crate) include_usage: bool,
1291 #[serde(default)]
1292 pub(crate) continuous_usage_stats: bool,
1293}
1294
1295#[derive(Deserialize)]
1296pub(crate) struct CompletionRequest {
1297 pub(crate) model: Option<String>,
1298 #[serde(default)]
1299 pub(crate) ignore_eos: bool,
1300 #[serde(default)]
1301 pub(crate) prompt: String,
1302 #[serde(default)]
1303 pub(crate) stream: bool,
1304 #[serde(default)]
1305 pub(crate) echo: bool,
1306 pub(crate) max_tokens: Option<u32>,
1307 pub(crate) temperature: Option<f32>,
1308 pub(crate) top_p: Option<f32>,
1309 pub(crate) top_k: Option<u32>,
1310 pub(crate) seed: Option<u64>,
1311 pub(crate) presence_penalty: Option<f32>,
1312 pub(crate) frequency_penalty: Option<f32>,
1313 #[serde(alias = "repetition_penalty")]
1314 pub(crate) repeat_penalty: Option<f32>,
1315 pub(crate) min_p: Option<f32>,
1316 pub(crate) logprobs: Option<u32>,
1317 pub(crate) suffix: Option<String>,
1318 pub(crate) fim_mode: Option<String>,
1319}
1320
1321pub(crate) const DEFAULT_TOP_K: u32 = 40;
1322
1323pub(crate) const DEFAULT_REPEAT_PENALTY: f32 = 1.0;
1324
1325#[cfg(test)]
1326mod embedding_sampling_tests {
1327 use super::*;
1328
1329 fn policy() -> SamplingOverrides {
1330 SamplingOverrides {
1331 fallback: Some(SamplingFallback {
1332 temperature: 0.0, repeat_penalty: 1.07,
1333 top_p: 0.82, top_k: 17, min_p: 0.03,
1334 frequency_penalty_as_repeat: true,
1335 }),
1336 ..Default::default()
1337 }
1338 }
1339
1340 #[test]
1341 fn omitted_parameters_and_props_share_embedding_defaults() {
1342 let m = ModelSamplingDefaults::default();
1343 let op = policy();
1344 let p = advertised_generation_defaults(&m, &op);
1345 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p), (0.0, 0.82, 17, 0.03));
1346 assert_eq!(repeat_penalty_for(None, &op, &m), 1.07);
1347 let original = advertised_generation_defaults(&m, &SamplingOverrides::default());
1348 assert_eq!((original.temperature, original.top_p, original.top_k, original.min_p), (1.0, 1.0, 40, 0.0));
1349 assert_eq!(repeat_penalty_for(None, &SamplingOverrides::default(), &m), 1.0);
1350 }
1351
1352 #[test]
1353 fn model_truncation_wins_as_a_set_but_not_model_temperature_or_repeat() {
1354 for model in [
1355 ModelSamplingDefaults { top_p: Some(0.95), ..Default::default() },
1356 ModelSamplingDefaults { top_k: Some(9), ..Default::default() },
1357 ModelSamplingDefaults { min_p: Some(0.1), ..Default::default() },
1358 ] {
1359 let model = ModelSamplingDefaults { temperature: Some(0.7), repetition_penalty: Some(1.2), ..model };
1360 let p = advertised_generation_defaults(&model, &policy());
1361 assert_eq!(p.temperature, 0.0);
1362 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)));
1363 assert_eq!(repeat_penalty_for(None, &policy(), &model), 1.07);
1364 }
1365 }
1366
1367 #[test]
1368 fn caller_and_operator_values_override_embedding_without_merging_truncation() {
1369 let m = ModelSamplingDefaults { top_k: Some(90), ..Default::default() };
1370 let op = SamplingOverrides { temperature: Some(0.6), top_k: Some(12), repeat_penalty: Some(1.3), ..policy() };
1371 let p = advertised_generation_defaults(&m, &op);
1372 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p), (0.6, 1.0, 12, 0.0));
1373 assert_eq!(repeat_penalty_for(None, &op, &m), 1.3);
1374 let p = params_of_for(Some(0.2), Some(1.0), None, None, Some(42), &m, &op);
1375 assert_eq!((p.temperature, p.top_p, p.top_k, p.min_p, p.seed), (0.2, 1.0, 0, 0.0, 42));
1376 assert_eq!(repeat_penalty_for(Some(1.0), &op, &m), 1.0);
1377 }
1378
1379 #[test]
1380 fn penalty_mapping_is_opt_in_and_respects_explicit_parameters() {
1381 let m = ModelSamplingDefaults::default();
1382 for (presence, frequency, repeat, expected) in [
1383 (None, None, None, (0.0, 0.0, 1.07)),
1384 (None, Some(0.2), None, (0.0, 0.0, 1.2)),
1385 (None, Some(0.0), None, (0.0, 0.0, 1.0)),
1386 (Some(0.0), Some(0.2), None, (0.0, 0.2, 1.0)),
1387 (Some(0.3), None, None, (0.3, 0.0, 1.0)),
1388 (None, Some(0.2), Some(1.1), (0.0, 0.2, 1.1)),
1389 ] {
1390 let e = extras_of(presence, frequency, repeat, &policy(), &m);
1391 assert_eq!((e.presence_penalty, e.frequency_penalty, e.repeat_penalty), expected);
1392 }
1393 let e = extras_of(None, Some(0.2), None, &SamplingOverrides::default(), &m);
1394 assert_eq!((e.frequency_penalty, e.repeat_penalty), (0.2, 1.0));
1395 }
1396}
1397
1398pub(crate) fn advertised_generation_defaults(
1399 model: &ModelSamplingDefaults,
1400 op: &SamplingOverrides,
1401) -> GenParams {
1402 params_of_for(None, None, None, None, None, model, op)
1403}
1404
1405pub(crate) fn repeat_penalty_for(
1406 caller: Option<f32>,
1407 op: &SamplingOverrides,
1408 model: &ModelSamplingDefaults,
1409) -> f32 {
1410 caller
1411 .or(op.repeat_penalty)
1412 .or(op.fallback.map(|f| f.repeat_penalty))
1413 .or(model.repetition_penalty)
1414 .unwrap_or(DEFAULT_REPEAT_PENALTY)
1415}
1416
1417pub(crate) fn draw_seed_if_unseeded(params: &mut GenParams, client_seed: Option<u64>) -> bool {
1418 if client_seed.is_some() || params.temperature <= 0.0 {
1419 return false;
1420 }
1421 params.seed = fresh_seed();
1422 true
1423}
1424
1425fn fresh_seed() -> u64 {
1426 use std::hash::{BuildHasher, Hasher};
1427 let h = std::collections::hash_map::RandomState::new()
1428 .build_hasher()
1429 .finish();
1430 if h == 0 {
1431 1
1432 } else {
1433 h
1434 }
1435}
1436
1437pub(crate) fn params_of_for(
1438 temperature: Option<f32>,
1439 top_p: Option<f32>,
1440 top_k: Option<u32>,
1441 min_p: Option<f32>,
1442 seed: Option<u64>,
1443 model: &ModelSamplingDefaults,
1444 op: &SamplingOverrides,
1445) -> GenParams {
1446 let model_temperature = match model.do_sample {
1447 Some(false) => Some(0.0),
1448 _ => model.temperature,
1449 };
1450 let chosen = temperature.is_some() || op.temperature.is_some();
1451 let temperature = temperature
1452 .or(op.temperature)
1453 .or(op.fallback.map(|f| f.temperature))
1454 .or(model_temperature)
1455 .unwrap_or(1.0);
1456 let temperature = match op.spec_max_temperature {
1457 Some(cap) if !chosen => temperature.min(cap),
1458 _ => temperature,
1459 };
1460
1461 let caller_truncates = top_p.is_some() || top_k.is_some() || min_p.is_some();
1462 let model_truncates = model.top_p.is_some() || model.top_k.is_some() || model.min_p.is_some();
1463 let (eff_top_p, eff_top_k, eff_min_p) = if caller_truncates {
1464 (top_p, top_k, min_p)
1465 } else if op.truncates() {
1466 (op.top_p, op.top_k, op.min_p)
1467 } else if model_truncates {
1468 (model.top_p, model.top_k, model.min_p)
1469 } else if let Some(f) = op.fallback {
1470 (Some(f.top_p), Some(f.top_k), Some(f.min_p))
1471 } else if temperature > 0.0 {
1472 (None, Some(DEFAULT_TOP_K), None)
1473 } else {
1474 (None, None, None)
1475 };
1476 GenParams {
1477 temperature,
1478 top_p: eff_top_p.unwrap_or(1.0),
1479 min_p: eff_min_p.unwrap_or(0.0),
1480 top_k: eff_top_k.unwrap_or(0),
1481 seed: seed.unwrap_or(0),
1482 }
1483}
1484
1485#[derive(Deserialize, Default)]
1486pub(crate) struct ChatMessage {
1487 pub(crate) role: String,
1488 #[serde(default)]
1489 pub(crate) content: Option<serde_json::Value>,
1490 #[serde(default)]
1491 pub(crate) tool_calls: Vec<ReqToolCall>,
1492 #[serde(default)]
1493 pub(crate) tool_call_id: Option<String>,
1494 #[serde(default)]
1495 pub(crate) name: Option<String>,
1496 #[serde(default)]
1497 pub(crate) reasoning_content: Option<String>,
1498 #[serde(default)]
1499 pub(crate) reasoning: Option<String>,
1500}
1501
1502pub(crate) fn reasoning_of(m: &ChatMessage, role: u32) -> Option<String> {
1503 if role != role::ASSISTANT {
1504 return None;
1505 }
1506 m.reasoning_content
1507 .as_deref()
1508 .or(m.reasoning.as_deref())
1509 .map(str::trim)
1510 .filter(|s| !s.is_empty())
1511 .map(str::to_string)
1512}
1513
1514#[derive(Clone, Copy, PartialEq, Eq)]
1515pub(crate) enum MediaKind {
1516 Image,
1517 Audio,
1518}
1519
1520fn require_media(daemon: &Daemon, kind: MediaKind, param: &str) -> Result<(), DaemonError> {
1521 match kind {
1522 MediaKind::Image => daemon.require("modalities", "image_encode", "unsupported_input", Some(param), "does not accept images"),
1523 MediaKind::Audio => daemon.require("modalities", "gemma_audio_encode", "unsupported_input", Some(param), "does not accept audio"),
1524 }
1525}
1526
1527fn content_param(messages: &[ChatMessage], m: &ChatMessage) -> String {
1528 match messages.iter().position(|x| std::ptr::eq(x, m)) {
1529 Some(i) => format!("messages[{i}].content"),
1530 None => "messages".to_string(),
1531 }
1532}
1533
1534pub(crate) struct SplitContent {
1535 pub(crate) text: String,
1536 pub(crate) images: Vec<(String, Vec<u8>, MediaKind)>,
1537}
1538
1539pub(crate) fn absorbable_system_text(m: &ChatMessage) -> Result<Option<String>, DaemonError> {
1540 if m.role != "system" {
1541 return Ok(None);
1542 }
1543 let split = split_content(&m.content)?;
1544 Ok(split.images.is_empty().then_some(split.text))
1545}
1546
1547pub(crate) fn split_content(v: &Option<serde_json::Value>) -> Result<SplitContent, DaemonError> {
1548 let mut out = SplitContent {
1549 text: String::new(),
1550 images: Vec::new(),
1551 };
1552 match v {
1553 None => {}
1554 Some(serde_json::Value::String(s)) => out.text = s.clone(),
1555 Some(serde_json::Value::Array(parts)) => {
1556 for p in parts {
1557 match p.get("type").and_then(|t| t.as_str()) {
1558 Some("text") => {
1559 if let Some(t) = p.get("text").and_then(|t| t.as_str()) {
1560 out.text.push_str(t);
1561 }
1562 }
1563 Some("image_url") => {
1564 let url = p
1565 .get("image_url")
1566 .and_then(|u| u.get("url"))
1567 .and_then(|u| u.as_str())
1568 .unwrap_or("");
1569 let Some(rest) = url.strip_prefix("data:") else {
1570 return Err(DaemonError::Protocol(
1571 "image_url must be a data: URL (remote images are not fetched)",
1572 ));
1573 };
1574 let Some((_, b64)) = rest.split_once(";base64,") else {
1575 return Err(DaemonError::Protocol("image_url data URL must be base64"));
1576 };
1577 let bytes = base64_decode(b64)?;
1578 out.images
1579 .push((std::mem::take(&mut out.text), bytes, MediaKind::Image));
1580 }
1581 Some("input_audio") => {
1582 let ia = p.get("input_audio");
1583 let fmt = ia
1584 .and_then(|a| a.get("format"))
1585 .and_then(|f| f.as_str())
1586 .unwrap_or("wav");
1587 if fmt != "wav" {
1588 return Err(DaemonError::Protocol(
1589 "input_audio format must be wav (16-bit PCM or float32)",
1590 ));
1591 }
1592 let b64 = ia
1593 .and_then(|a| a.get("data"))
1594 .and_then(|d| d.as_str())
1595 .ok_or(DaemonError::Protocol("input_audio requires base64 data"))?;
1596 let bytes = base64_decode(b64)?;
1597 out.images
1598 .push((std::mem::take(&mut out.text), bytes, MediaKind::Audio));
1599 }
1600 _ => {}
1601 }
1602 }
1603 }
1604 Some(_) => {
1605 return Err(DaemonError::Protocol(
1606 "message content must be a string or parts",
1607 ))
1608 }
1609 }
1610 Ok(out)
1611}
1612
1613#[derive(Deserialize)]
1614pub(crate) struct ReqToolCall {
1615 #[serde(default)]
1616 pub(crate) id: Option<String>,
1617 pub(crate) function: Option<ReqFunction>,
1618}
1619
1620#[derive(Deserialize)]
1621pub(crate) struct ReqFunction {
1622 pub(crate) name: String,
1623 #[serde(default)]
1624 pub(crate) arguments: serde_json::Value,
1625}
1626
1627pub(crate) fn build_session(
1628 daemon: &Daemon,
1629 req: &ChatRequest,
1630 op: &SamplingOverrides,
1631 model: &ModelSamplingDefaults,
1632) -> Result<(u64, bool), DaemonError> {
1633 if let Err(why) = daemon.validate_template_kwargs(&req.template_kwargs()) {
1634 return Err(DaemonError::Constraint(why));
1635 }
1636 let mut params = params_of_for(
1637 req.temperature,
1638 req.top_p,
1639 req.top_k,
1640 req.min_p,
1641 req.seed,
1642 model,
1643 &op.for_daemon(daemon),
1644 );
1645 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
1646
1647 if let Some(c) = req.continues {
1648 if !daemon.renders_per_message() {
1651 return Err(DaemonError::Protocol(
1652 "this model's chat template renders whole conversations; a stored one is not continued by appending",
1653 ));
1654 }
1655 let session = daemon.fork(c.session, c.at_event, Some(params))?;
1656 let last_query = req.messages.iter().rposition(|m| m.role == "user");
1659 for (i, m) in req.messages.iter().enumerate() {
1660 append_chat_message(daemon, session, req, m, last_query.is_none_or(|q| i > q))?;
1661 }
1662 return Ok((session, seed_drawn));
1663 }
1664
1665 let session = daemon.create(None, params)?;
1666
1667 if !daemon.renders_per_message() {
1668 let mut msgs: Vec<crate::codec::ChatMessage> = Vec::with_capacity(req.messages.len());
1669 for m in &req.messages {
1670 let split = split_content(&m.content)?;
1671 let role = match m.role.as_str() {
1672 "system" => role::SYSTEM,
1673 "assistant" => role::ASSISTANT,
1674 "tool" => role::TOOL,
1675 _ => role::USER,
1676 };
1677 if !split.images.is_empty() {
1678 let mut parts: Vec<crate::codec::ContentPart> = Vec::new();
1679 let trailing = split.text;
1680 for (pre, bytes, kind) in split.images {
1681 require_media(daemon, kind, &content_param(&req.messages, m))?;
1682 if !pre.is_empty() {
1683 parts.push(crate::codec::ContentPart::Text(pre));
1684 }
1685 let hash = daemon.put_media(&bytes)?;
1686 parts.push(match kind {
1687 MediaKind::Image => crate::codec::ContentPart::Image { blob: hash },
1688 MediaKind::Audio => crate::codec::ContentPart::Audio { blob: hash },
1689 });
1690 }
1691 if !trailing.is_empty() {
1692 parts.push(crate::codec::ContentPart::Text(trailing));
1693 }
1694 let mut msg = crate::codec::ChatMessage::new(role, String::new());
1695 msg.parts = parts;
1696 msg.reasoning = reasoning_of(m, role);
1697 msgs.push(msg);
1698 continue;
1699 }
1700 let mut msg = crate::codec::ChatMessage::new(role, split.text);
1701 for c in &m.tool_calls {
1702 let Some(f) = &c.function else { continue };
1703 let args = match &f.arguments {
1704 serde_json::Value::String(s) => s.clone(),
1705 other => other.to_string(),
1706 };
1707 msg.tool_calls.push(crate::codec::ToolCallMsg {
1708 id: c.id.clone().unwrap_or_default(),
1709 name: f.name.clone(),
1710 arguments: args,
1711 });
1712 }
1713 msg.reasoning = reasoning_of(m, role);
1714 if role == role::TOOL {
1715 msg.tool_call_id = m.tool_call_id.clone();
1716 msg.name = m.name.clone();
1717 }
1718 msgs.push(msg);
1719 }
1720 let tools: Vec<String> = req.tools.iter().map(|t| t.to_string()).collect();
1721 daemon.append_conversation(session, &msgs, &tools, &req.template_kwargs())?;
1722 return Ok((session, seed_drawn));
1723 }
1724
1725 let (system, tool_jsons) = system_head(req)?;
1726 let rest = if system.is_some() { &req.messages[1..] } else { &req.messages[..] };
1727 daemon.append_system_full(session, system, tool_jsons, &req.template_kwargs())?;
1728 let last_query = req.messages.iter().rposition(|m| m.role == "user");
1730 let skipped = req.messages.len() - rest.len();
1731 for (i, m) in rest.iter().enumerate() {
1732 let after_query = last_query.is_some_and(|q| skipped + i > q);
1733 append_chat_message(daemon, session, req, m, after_query)?;
1734 }
1735 Ok((session, seed_drawn))
1736}
1737
1738pub(crate) fn system_head(req: &ChatRequest) -> Result<(Option<String>, Vec<String>), DaemonError> {
1742 let system = req.messages.first().map(absorbable_system_text).transpose()?.flatten();
1743 let tools = if !req.tools.is_empty() && ToolChoice::parse(&req.tool_choice).declares_tools() {
1744 req.tools.iter().map(|t| t.to_string()).collect()
1745 } else {
1746 Vec::new()
1747 };
1748 Ok((system, tools))
1749}
1750
1751fn append_chat_message(
1752 daemon: &Daemon,
1753 session: u64,
1754 req: &ChatRequest,
1755 m: &ChatMessage,
1756 after_query: bool,
1757) -> Result<(), DaemonError> {
1758 let split = split_content(&m.content)?;
1759 if !split.images.is_empty() {
1760 let r = match m.role.as_str() {
1761 "assistant" => role::ASSISTANT,
1762 "system" => role::SYSTEM,
1763 _ => role::USER,
1764 };
1765 let n = split.images.len();
1766 let mut trailing = split.text;
1767 for (i, (pre, bytes, kind)) in split.images.into_iter().enumerate() {
1768 require_media(daemon, kind, &content_param(&req.messages, m))?;
1769 let hash = daemon.put_media(&bytes)?;
1770 let post = if i + 1 == n {
1771 std::mem::take(&mut trailing)
1772 } else {
1773 String::new()
1774 };
1775 daemon.append_image(session, r, &hash, &pre, &post)?;
1776 }
1777 return Ok(());
1778 }
1779 let content = split.text;
1780 match m.role.as_str() {
1781 "assistant" if !m.tool_calls.is_empty() => {
1782 let calls: Vec<(String, String)> = m
1783 .tool_calls
1784 .iter()
1785 .filter_map(|c| c.function.as_ref())
1786 .map(|f| {
1787 let args = match &f.arguments {
1788 serde_json::Value::String(s) => s.clone(),
1789 v => v.to_string(),
1790 };
1791 (f.name.clone(), args)
1792 })
1793 .collect();
1794 daemon.append_assistant_with_tool_calls(session, content, calls, after_query)?;
1795 }
1796 "assistant" => {
1797 daemon.append_message(session, role::ASSISTANT, content)?;
1798 }
1799 "system" => {
1800 daemon.append_message(session, role::SYSTEM, content)?;
1801 }
1802 "tool" => {
1803 daemon.append_tool_history(session, content)?;
1804 }
1805 _ => {
1806 daemon.append_message(session, role::USER, content)?;
1807 }
1808 }
1809 Ok(())
1810}
1811
1812pub(crate) struct Collected {
1813 pub(crate) content: String,
1814 pub(crate) reasoning: String,
1815 pub(crate) tool_calls: Vec<(u64, String, String)>,
1816 pub(crate) unparsed_tool: String,
1817 pub(crate) broken_calls: Vec<(u64, String, String)>,
1818}
1819
1820pub(crate) fn promote_forced_tool_call(
1821 daemon: &Daemon,
1822 c: &mut Collected,
1823 session: u64,
1824 schemas: Option<&crate::codec::ToolSchemas>,
1825) {
1826 if !c.tool_calls.is_empty() {
1827 return;
1828 }
1829 let raw = c.content.trim().to_string();
1830 if raw.is_empty() {
1831 return;
1832 }
1833 if let Some((name, args)) = daemon.parse_tool_call_with(&raw, schemas) {
1834 c.tool_calls.push((session, name, args));
1835 c.content.clear();
1836 }
1837}
1838
1839pub(crate) fn claim_named_calls(daemon: &Daemon, c: &mut Collected, events: &[crate::CommittedEvent], choice: &ToolChoice) {
1840 if *choice == ToolChoice::None {
1841 return;
1842 }
1843 let mut left = String::new();
1844 for e in events {
1845 if let EventBody::ToolParseFailure { raw } = &e.body {
1846 match daemon.named_tool_call(raw) {
1847 Some((name, args)) => c.broken_calls.push((e.event_id, name, args)),
1848 None => left.push_str(raw),
1849 }
1850 }
1851 }
1852 c.unparsed_tool = left;
1853}
1854
1855pub(crate) fn collect(events: &[crate::CommittedEvent]) -> Collected {
1856 let mut c = Collected {
1857 content: String::new(),
1858 reasoning: String::new(),
1859 tool_calls: Vec::new(),
1860 unparsed_tool: String::new(),
1861 broken_calls: Vec::new(),
1862 };
1863 for e in events {
1864 match &e.body {
1865 EventBody::Generated {
1866 text, channel: ch, ..
1867 } => match *ch {
1868 channel::REASONING => c.reasoning.push_str(text),
1869 channel::TOOL_CALL => {}
1870 _ => c.content.push_str(text),
1871 },
1872 EventBody::ToolUse { name, arguments } => {
1873 c.tool_calls
1874 .push((e.event_id, name.clone(), arguments.clone()));
1875 }
1876 EventBody::ToolParseFailure { raw } => c.unparsed_tool.push_str(raw),
1877 _ => {}
1878 }
1879 }
1880 c
1881}
1882
1883pub(crate) fn finish_reason(
1884 fin: u32,
1885 produced: u32,
1886 max_tokens: u32,
1887 had_tool_calls: bool,
1888) -> &'static str {
1889 let cut = match fin {
1890 finish::LENGTH => true,
1891 finish::EOS | finish::CANCELLED => false,
1892 _ => produced >= max_tokens,
1893 };
1894 if cut {
1895 "length"
1896 } else if had_tool_calls {
1897 "tool_calls"
1898 } else {
1899 "stop"
1900 }
1901}
1902
1903pub(crate) fn prompt_len(daemon: &Daemon, session: u64) -> u32 {
1904 daemon
1905 .store()
1906 .lock()
1907 .expect("store")
1908 .session(session)
1909 .map(|s| s.tokens.len().min(u32::MAX as usize) as u32)
1910 .unwrap_or(0)
1911}
1912
1913pub(crate) fn next_id() -> String {
1914 use std::sync::atomic::{AtomicU64, Ordering};
1915 static N: AtomicU64 = AtomicU64::new(1);
1916 format!("chatcmpl-superfluid-{}", N.fetch_add(1, Ordering::Relaxed))
1917}
1918
1919pub(crate) fn unix_now() -> u64 {
1920 std::time::SystemTime::now()
1921 .duration_since(std::time::UNIX_EPOCH)
1922 .map(|d| d.as_secs())
1923 .unwrap_or(0)
1924}
1925
1926#[cfg(test)]
1927mod access_log_rate_tests {
1928 use super::*;
1929 use std::time::Duration;
1930
1931 #[test]
1932 fn prefill_counts_only_what_was_prefilled() {
1933 let cases = [
1934 (12961u64, 0u64, 10444u64, "1241"),
1935 (14481, 12800, 5716, "294"),
1936 (15627, 14900, 5057, "144"),
1937 (16045, 15900, 2138, "68"),
1938 ];
1939 for (prompt, cached, ttft_ms, want) in cases {
1940 let got = prefill_rate(prompt, cached, Some(Duration::from_millis(ttft_ms)));
1941 assert_eq!(got, want, "prompt={prompt} cached={cached}");
1942 }
1943 }
1944
1945 #[test]
1946 fn a_fully_warm_prompt_prefilled_nothing() {
1947 assert_eq!(
1948 prefill_rate(4096, 4096, Some(Duration::from_millis(80))),
1949 "-"
1950 );
1951 assert_eq!(
1952 prefill_rate(4096, 9000, Some(Duration::from_millis(80))),
1953 "-"
1954 );
1955 }
1956
1957 #[test]
1958 fn a_fim_cache_hit_logs_its_prompt_as_cached() {
1959 let c = |cached| crate::Completion {
1960 text: String::new(),
1961 tokens: vec![1; 16],
1962 cached,
1963 expired: false,
1964 finish: superfluid_abi::finish::NONE,
1965 prompt_tokens: 21,
1966 };
1967 assert_eq!(fim_log_cached(&c(true)), 21);
1968 assert_eq!(
1969 prefill_rate(
1970 21,
1971 fim_log_cached(&c(true)),
1972 Some(Duration::from_micros(60))
1973 ),
1974 "-"
1975 );
1976 assert_eq!(fim_log_cached(&c(false)), 0);
1977 }
1978
1979 #[test]
1980 fn prefill_needs_a_ttft() {
1981 assert_eq!(prefill_rate(1024, 0, None), "-");
1982 assert_eq!(prefill_rate(1024, 0, Some(Duration::ZERO)), "-");
1983 }
1984
1985 fn run(d: &mut DecodeSpan, base: std::time::Instant, stamps: &[(u64, u64)]) {
1986 for (ms, produced) in stamps {
1987 d.stamp_at(base + Duration::from_millis(*ms), *produced);
1988 }
1989 d.finish();
1990 }
1991
1992 #[test]
1993 fn decode_is_the_inter_token_rate() {
1994 let base = std::time::Instant::now();
1995 let mut d = DecodeSpan::default();
1996 run(&mut d, base, &[(0, 1), (781, 200)]);
1997 let (tokens, span) = d.measured();
1998 assert_eq!(tokens, 199);
1999 assert_eq!(decode_rate(tokens, span), "254.8");
2000 }
2001
2002 #[test]
2003 fn decode_excludes_the_whole_first_commit() {
2004 let base = std::time::Instant::now();
2005 let mut d = DecodeSpan::default();
2006 run(&mut d, base, &[(0, 5), (1000, 100)]);
2007 assert_eq!(d.measured().0, 95, "the first commit's 5 predate the span");
2008 assert_eq!(decode_rate(d.measured().0, d.measured().1), "95.0");
2009 }
2010
2011 #[test]
2012 fn decode_spans_do_not_swallow_the_gaps_between_choices() {
2013 let base = std::time::Instant::now();
2014 let mut d = DecodeSpan::default();
2015 run(&mut d, base, &[(0, 1), (500, 51)]);
2016 run(&mut d, base, &[(4500, 1), (5000, 51)]);
2017 let (tokens, span) = d.measured();
2018 assert_eq!(tokens, 100);
2019 assert_eq!(span, Some(Duration::from_millis(1000)), "gap excluded");
2020 assert_eq!(decode_rate(tokens, span), "100.0");
2021 }
2022
2023 #[test]
2024 fn decode_counts_only_what_was_delivered() {
2025 let base = std::time::Instant::now();
2026 let mut d = DecodeSpan::default();
2027 run(&mut d, base, &[(0, 1), (500, 50)]);
2028 let (tokens, span) = d.measured();
2029 assert_eq!(tokens, 49, "the undelivered tail is usage's business");
2030 assert_eq!(decode_rate(tokens, span), "98.0");
2031 }
2032
2033 #[test]
2034 fn decode_declines_to_guess() {
2035 let base = std::time::Instant::now();
2036 let mut d = DecodeSpan::default();
2037 d.finish();
2038 assert_eq!(d.measured(), (0, None));
2039 assert_eq!(decode_rate(0, None), "-");
2040
2041 let mut one = DecodeSpan::default();
2042 run(&mut one, base, &[(0, 40)]);
2043 assert_eq!(decode_rate(one.measured().0, one.measured().1), "-");
2044
2045 assert_eq!(decode_rate(200, Some(Duration::from_millis(150))), "-");
2046 assert_eq!(decode_rate(100, None), "-");
2047 }
2048
2049 #[test]
2050 fn a_tick_split_by_a_channel_change_is_not_counted_against_the_next_tick() {
2051 let base = std::time::Instant::now();
2055 let mut d = DecodeSpan::default();
2056 run(&mut d, base, &[(0, 19), (9, 98), (801, 128)]);
2057 assert_eq!(decode_rate(d.measured().0, d.measured().1), "136.1", "events alone overstate it");
2058 d.add_measured((30, 0.792));
2059 assert_eq!(decode_rate(d.measured().0, d.measured().1), "37.9");
2060 }
2061
2062 #[test]
2063 fn a_generation_delivered_in_one_tick_has_no_decode_rate() {
2064 let base = std::time::Instant::now();
2065 let mut d = DecodeSpan::default();
2066 run(&mut d, base, &[(0, 128)]);
2067 d.add_measured((0, 0.0));
2068 assert_eq!(decode_rate(d.measured().0, d.measured().1), "-");
2069 }
2070
2071 #[test]
2072 fn a_one_tick_generation_split_by_a_channel_change_has_no_decode_rate() {
2073 let base = std::time::Instant::now();
2076 let mut d = DecodeSpan::default();
2077 run(&mut d, base, &[(0, 19), (9, 20)]);
2078 assert_eq!(decode_rate(d.measured().0, d.measured().1), "111.1", "events alone");
2079 d.add_measured((0, 0.0));
2080 assert_eq!(d.measured(), (0, None), "the scheduler saw no tick after the first");
2081 }
2082
2083 #[test]
2084 fn measured_decode_adds_up_over_choices_and_ignores_empty_ones() {
2085 let mut d = DecodeSpan::default();
2086 d.add_measured((63, 0.5));
2087 d.add_measured((0, 0.0));
2088 d.add_measured((63, 0.5));
2089 assert_eq!(d.measured(), (126, Some(Duration::from_secs(1))));
2090 assert_eq!(decode_rate(d.measured().0, d.measured().1), "126.0");
2091 }
2092}
2093
2094#[cfg(test)]
2095mod key_order_tests {
2096 use super::ChatRequest;
2097
2098 #[test]
2099 fn a_tool_keeps_the_key_order_it_was_sent_in() {
2100 let req: ChatRequest = serde_json::from_str(
2101 r#"{"messages":[],"tools":[{"type":"function","function":{"name":"replace_text",
2102 "parameters":{"type":"object","properties":{"path":{"type":"string"},
2103 "old_text":{"type":"string"},"new_text":{"type":"string"}}}}}]}"#,
2104 )
2105 .expect("request");
2106 let t = req.tools[0].to_string();
2107 let at = |k: &str| t.find(k).unwrap_or_else(|| panic!("{k} missing: {t}"));
2108 assert!(at("\"type\"") < at("\"function\""), "{t}");
2109 assert!(
2110 at("\"path\"") < at("\"old_text\"") && at("\"old_text\"") < at("\"new_text\""),
2111 "{t}"
2112 );
2113 }
2114}
2115
2116#[cfg(test)]
2117mod system_absorption_tests {
2118 use super::*;
2119
2120 #[test]
2121 fn assistant_reasoning_is_carried_and_other_roles_are_not() {
2122 let mut m = msg("assistant", serde_json::Value::Null);
2123 m.reasoning_content = Some(" plan ".into());
2124 assert_eq!(reasoning_of(&m, role::ASSISTANT), Some("plan".into()));
2125 m.reasoning_content = None;
2126 m.reasoning = Some("alt".into());
2127 assert_eq!(
2128 reasoning_of(&m, role::ASSISTANT),
2129 Some("alt".into()),
2130 "the other spelling"
2131 );
2132 m.reasoning = Some(" ".into());
2133 assert_eq!(
2134 reasoning_of(&m, role::ASSISTANT),
2135 None,
2136 "blank reasoning is none"
2137 );
2138 let mut u = msg("user", serde_json::json!("hi"));
2139 u.reasoning_content = Some("x".into());
2140 assert_eq!(
2141 reasoning_of(&u, role::USER),
2142 None,
2143 "only assistant turns carry reasoning"
2144 );
2145 }
2146
2147 fn msg(role: &str, content: serde_json::Value) -> ChatMessage {
2148 ChatMessage {
2149 role: role.to_string(),
2150 content: Some(content),
2151 tool_calls: Vec::new(),
2152 tool_call_id: None,
2153 name: None,
2154 reasoning_content: None,
2155 reasoning: None,
2156 }
2157 }
2158
2159 fn image_part() -> serde_json::Value {
2160 serde_json::json!({
2161 "type": "image_url",
2162 "image_url": {"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="}
2163 })
2164 }
2165
2166 #[test]
2167 fn text_only_system_message_is_absorbed() {
2168 let m = msg("system", serde_json::json!("you are helpful"));
2169 assert_eq!(
2170 absorbable_system_text(&m).expect("split"),
2171 Some("you are helpful".to_string()),
2172 "a text-only system message folds into the scaffold"
2173 );
2174 }
2175
2176 #[test]
2177 fn multimodal_system_message_is_left_for_the_media_loop() {
2178 let m = msg(
2179 "system",
2180 serde_json::json!([{"type": "text", "text": "look at this"}, image_part()]),
2181 );
2182 let split = split_content(&m.content).expect("split");
2183 assert_eq!(split.images.len(), 1, "fixture must carry one image part");
2184
2185 assert_eq!(
2186 absorbable_system_text(&m).expect("split"),
2187 None,
2188 "a system message with media must NOT be absorbed — its images \
2189 would be dropped on the floor"
2190 );
2191 }
2192
2193 #[test]
2194 fn audio_system_message_is_left_for_the_media_loop() {
2195 let m = msg(
2196 "system",
2197 serde_json::json!([
2198 {"type": "text", "text": "listen"},
2199 {"type": "input_audio", "input_audio": {"data": "UklGRiQAAABXQVZF", "format": "wav"}}
2200 ]),
2201 );
2202 let split = split_content(&m.content).expect("split");
2203 assert_eq!(split.images.len(), 1, "fixture must carry one audio part");
2204 assert_eq!(absorbable_system_text(&m).expect("split"), None);
2205 }
2206
2207 #[test]
2208 fn non_system_roles_are_never_absorbed() {
2209 for role in ["user", "assistant", "tool", "developer"] {
2210 let m = msg(role, serde_json::json!("hello"));
2211 assert_eq!(
2212 absorbable_system_text(&m).expect("split"),
2213 None,
2214 "{role} must not be absorbed into the system scaffold"
2215 );
2216 }
2217 }
2218}
2219
2220pub(crate) enum RunError {
2221 Daemon(DaemonError),
2222 Worker(String),
2223}
2224impl From<DaemonError> for RunError {
2225 fn from(error: DaemonError) -> Self {
2226 Self::Daemon(error)
2227 }
2228}
2229
2230pub(crate) async fn completions(
2231 daemon: Arc<Daemon>,
2232 model_name: String,
2233 req: CompletionRequest,
2234 policy: RequestPolicy,
2235 hold: Option<crate::keypolicy::ClientKey>,
2236 hooks: crate::keepalive::Hooks,
2237) -> Result<Generation, RunError> {
2238 let qos = policy.qos;
2239 let max_tokens = policy
2240 .limits
2241 .resolve(req.max_tokens, || daemon.tokenize(&req.prompt).len() as u32);
2242 let id = format!(
2243 "cmpl-superfluid-{}",
2244 next_id().rsplit('-').next().unwrap_or("0")
2245 );
2246 let created = unix_now();
2247 let model_defaults = {
2248 let d = daemon.clone();
2249 tokio::task::spawn_blocking(move || {
2250 crate::with_sync_class(Some(qos.class), || d.model_sampling_defaults())
2251 })
2252 .await
2253 .unwrap_or_default()
2254 };
2255 let mut params = params_of_for(
2256 req.temperature,
2257 req.top_p,
2258 req.top_k,
2259 req.min_p,
2260 req.seed,
2261 &model_defaults,
2262 &policy.sampling.for_daemon(&daemon),
2263 );
2264 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
2265 let mut extras = extras_of(
2266 req.presence_penalty,
2267 req.frequency_penalty,
2268 req.repeat_penalty,
2269 &policy.sampling,
2270 &model_defaults,
2271 );
2272 extras.seed_drawn = seed_drawn;
2273 let want_logprobs = req.logprobs.is_some();
2274 extras.want_logprobs = want_logprobs;
2275 extras.top_logprobs = req.logprobs.unwrap_or(0).min(20) as u8;
2276 extras.ignore_eos = req.ignore_eos;
2277 let echo_offset = if req.echo { req.prompt.len() } else { 0 };
2278
2279 if req.stream {
2280 let body = completion_stream(daemon, req, max_tokens, params, extras, qos, hold).await?;
2281 return Ok(Generation {
2282 kind: Kind::Completion,
2283 id,
2284 model: model_name,
2285 created,
2286 warm_header: None,
2287 body,
2288 });
2289 }
2290
2291 let daemon_lp = Arc::clone(&daemon);
2292 let model_log = model_name.clone();
2293 let out = blocking(qos, hold, move || {
2294 let mut hooks = hooks;
2295 let t0 = std::time::Instant::now();
2296 let mut decode = DecodeSpan::default();
2297 let mut produced = 0u64;
2298 let session = daemon.create(None, params)?;
2299 qos.apply(&daemon, session)?;
2300 daemon.append(session, Some(req.prompt.clone()), Vec::new())?;
2301 if hooks.is_on() {
2302 daemon.session_fits(session, None)?;
2303 }
2304 hooks.admitted(&daemon, session);
2305 hooks.check()?;
2306 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
2307 hooks.cancel_if_gone();
2308 if let EventBody::Generated { span, .. } = &ev.body {
2309 if !span.is_empty() {
2310 produced += span.len() as u64;
2311 decode.stamp(produced);
2312 }
2313 }
2314 Ok(())
2315 })?;
2316 let first_tok = decode.first_token();
2317 decode.finish();
2318 decode.add_measured(out.decode);
2319 let c = collect(&out.events);
2320 let usage = usage(&daemon, session, out.tokens_generated, out.warm_prefix);
2321 access_log(
2322 "POST /v1/completions",
2323 &model_log,
2324 usage.prompt.unwrap_or(0),
2325 out.warm_prefix,
2326 out.tokens_generated as u64,
2327 GenTiming {
2328 t0,
2329 first: first_tok,
2330 decode,
2331 },
2332 finish_reason(out.finish, out.tokens_generated, max_tokens, false),
2333 );
2334 let mut text = String::new();
2335 if req.echo {
2336 text.push_str(&req.prompt);
2337 }
2338 text.push_str(&c.content);
2339 text.push_str(&c.reasoning);
2340 text.push_str(&c.unparsed_tool);
2341 Ok::<_, DaemonError>((out, text, usage))
2342 })
2343 .await;
2344 let (out, text, usage) = out?;
2345 let logprobs = want_logprobs.then(|| decode_logprobs(&daemon_lp, &out.logprobs, echo_offset));
2346 let mut frame = Frame::text(
2347 text,
2348 logprobs,
2349 Some(finish_reason(
2350 out.finish,
2351 out.tokens_generated,
2352 max_tokens,
2353 false,
2354 )),
2355 );
2356 frame.usage = Some(usage);
2357 Ok(Generation {
2358 kind: Kind::Completion,
2359 id,
2360 model: model_name,
2361 created,
2362 warm_header: None,
2363 body: Body::Complete(frame),
2364 })
2365}
2366
2367#[allow(clippy::too_many_arguments)]
2368async fn completion_stream(
2369 daemon: Arc<Daemon>,
2370 req: CompletionRequest,
2371 max_tokens: u32,
2372 params: GenParams,
2373 extras: crate::scheduler::GenExtras,
2374 qos: RequestQos,
2375 hold: Option<crate::keypolicy::ClientKey>,
2376) -> Result<Body, RunError> {
2377 let want_logprobs = req.logprobs.is_some();
2378 let admitted_daemon = Arc::clone(&daemon);
2379 let (req, session) = blocking(qos, hold.clone(), move || {
2380 let session = admitted_daemon.create(None, params)?;
2381 qos.apply(&admitted_daemon, session)?;
2382 admitted_daemon.append(session, Some(req.prompt.clone()), Vec::new())?;
2383 admitted_daemon.session_fits(session, None)?;
2384 Ok((req, session))
2385 })
2386 .await?;
2387 let rx = spawn_stream(qos, hold, move |tx| {
2388 let _closed = crate::keepalive::cancel_when_closed(&daemon, session, tx);
2389 let send = |v: Frame| {
2390 tx.blocking_send(Ok(v))
2391 .map_err(|_| DaemonError::Protocol("generation consumer gone"))
2392 };
2393 let lp_q: std::rc::Rc<
2394 std::cell::RefCell<std::collections::VecDeque<crate::scheduler::TokenLogprob>>,
2395 > = std::rc::Rc::new(std::cell::RefCell::new(std::collections::VecDeque::new()));
2396 let lp_fill = std::rc::Rc::clone(&lp_q);
2397 let mut offset: usize = 0;
2398 let run = || -> Result<(), DaemonError> {
2399 if req.echo {
2400 offset = req.prompt.len();
2401 send(Frame::text(&req.prompt, None, None))?;
2402 }
2403 let out = daemon.generate_streaming_ex_lp(
2404 session,
2405 max_tokens,
2406 extras,
2407 |ev| {
2408 if let EventBody::Generated {
2409 text,
2410 channel: ch,
2411 span,
2412 ..
2413 } = &ev.body
2414 {
2415 let these: Vec<crate::scheduler::TokenLogprob> = if want_logprobs {
2416 let mut q = lp_q.borrow_mut();
2417 (0..span.len()).filter_map(|_| q.pop_front()).collect()
2418 } else {
2419 Vec::new()
2420 };
2421 if !text.is_empty() && *ch != channel::TOOL_CALL {
2422 let lp = if want_logprobs {
2423 let v = Some(decode_logprobs(&daemon, &these, offset));
2424 offset += text.len();
2425 v
2426 } else {
2427 None
2428 };
2429 return send(Frame::text(text, lp, None));
2430 }
2431 }
2432 Ok(())
2433 },
2434 |items| {
2435 lp_fill.borrow_mut().extend(items.iter().cloned());
2436 },
2437 )?;
2438 let fin = finish_reason(out.finish, out.tokens_generated, max_tokens, false);
2439 let mut terminal = Frame::text("", None, Some(fin));
2440 terminal.usage = Some(usage(
2441 &daemon,
2442 session,
2443 out.tokens_generated,
2444 out.warm_prefix,
2445 ));
2446 send(terminal)
2447 };
2448 run()
2449 });
2450 Ok(Body::Stream(rx))
2451}
2452pub(crate) async fn chat(
2453 daemon: Arc<Daemon>,
2454 model_name: String,
2455 req: ChatRequest,
2456 policy: RequestPolicy,
2457 hold: Option<crate::keypolicy::ClientKey>,
2458 hooks: crate::keepalive::Hooks,
2459) -> Result<Generation, RunError> {
2460 let qos = policy.qos;
2461 let explicit_max = req.max_completion_tokens.or(req.max_tokens);
2462 let id = next_id();
2463 let created = unix_now();
2464
2465 if req.stream {
2466 let body = chat_stream(daemon, model_name.clone(), req, policy, explicit_max, hold).await?;
2467 return Ok(Generation {
2468 kind: Kind::Chat,
2469 id,
2470 model: model_name,
2471 created,
2472 warm_header: None,
2473 body,
2474 });
2475 }
2476
2477 let n = req.n.unwrap_or(1).max(1);
2478 let route = req.route();
2479 let stops = req.stop.list();
2480 let model_log = model_name.clone();
2481 let out = blocking(qos, hold, move || {
2482 let mut hooks = hooks;
2483 let t0 = std::time::Instant::now();
2484 let mut decode = DecodeSpan::default();
2485 let mut ttft_first: Option<std::time::Instant> = None;
2486 let mut last_finish = "-";
2487 let model_defaults = daemon.model_sampling_defaults();
2488 let mut extras = extras_of(
2489 req.presence_penalty,
2490 req.frequency_penalty,
2491 req.repeat_penalty,
2492 &policy.sampling,
2493 &model_defaults,
2494 );
2495 let choice = ToolChoice::parse(&req.tool_choice);
2496 if choice.forces_call() {
2497 extras.open_channel = crate::wal::channel::TOOL_CALL;
2498 }
2499 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
2500 extras.tool_schemas = tool_schemas.clone();
2501 let handles = RequestHandles::new(
2502 &daemon,
2503 grammar_handle_for(&daemon, &req, &choice)?,
2504 logit_bias_handle_of(&daemon, &req.logit_bias),
2505 );
2506 handles.apply(&mut extras);
2507 let want_logprobs = req.logprobs.unwrap_or(false);
2508 extras.want_logprobs = want_logprobs;
2509 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
2510 extras.thinking = req.thinking();
2511 extras.ignore_eos = req.ignore_eos;
2512 let cancels = daemon.cancel_registry();
2513 let RequestPolicy {
2514 limits,
2515 sampling: op,
2516 qos,
2517 } = policy;
2518 let mut choices = Vec::with_capacity(n as usize);
2519 let mut completion_tokens = 0u64;
2520 let mut prompt_tokens = 0u64;
2521 let mut warm = 0u64;
2522 let mut warm_first: Option<u64> = None;
2523 let mut spec = (0u64, 0u64);
2524 let mut last_session = None;
2525 for i in 0..n {
2526 if i > 0 && hooks.gone() {
2527 break;
2528 }
2529 let (session, seed_drawn) = build_session(&daemon, &req, &op, &model_defaults)?;
2530 last_session = Some(session);
2531 let mut extras = extras.clone();
2532 extras.seed_drawn = seed_drawn;
2533 qos.apply(&daemon, session)?;
2534 if hooks.is_on() {
2535 daemon.session_fits(session, req.thinking())?;
2536 }
2537 hooks.admitted(&daemon, session);
2538 hooks.check()?;
2539 let max_tokens = limits.resolve(explicit_max, || prompt_len(&daemon, session));
2540 let mut acc = String::new();
2541 let mut produced = 0u64;
2542 let stops = stops.clone();
2543 let cancels = std::sync::Arc::clone(&cancels);
2544 let mut stopped = false;
2545 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
2546 hooks.cancel_if_gone();
2547 if let EventBody::Generated {
2548 text,
2549 channel: ch,
2550 span,
2551 ..
2552 } = &ev.body
2553 {
2554 if !span.is_empty() {
2555 produced += span.len() as u64;
2556 decode.stamp(produced);
2557 }
2558 if *ch == channel::TEXT && !stops.is_empty() && !stopped {
2559 acc.push_str(text);
2560 if stops.iter().any(|s| acc.contains(s.as_str())) {
2561 cancels.cancel(session);
2562 stopped = true;
2563 }
2564 }
2565 }
2566 Ok(())
2567 })?;
2568 ttft_first = ttft_first.or_else(|| decode.first_token());
2569 decode.finish();
2570 decode.add_measured(out.decode);
2571 let mut c = collect(&out.events);
2572 claim_named_calls(&daemon, &mut c, &out.events, &choice);
2573 if choice.forces_call() {
2574 promote_forced_tool_call(&daemon, &mut c, session, tool_schemas.as_deref());
2575 }
2576 let (content, hit_stop) = truncate_at_stop(&c.content, &stops);
2577 c.content = content;
2578 let usage = usage(&daemon, session, out.tokens_generated, out.warm_prefix);
2579 completion_tokens += usage.completion;
2580 spec.0 += out.spec.0;
2581 spec.1 += out.spec.1;
2582 prompt_tokens = usage.prompt.unwrap_or(prompt_tokens);
2583 warm = warm.max(out.warm_prefix);
2584 warm_first.get_or_insert(out.warm_prefix);
2585 let finish = if !c.broken_calls.is_empty() {
2586 "length"
2587 } else if hit_stop {
2588 "stop"
2589 } else {
2590 finish_reason(
2591 out.finish,
2592 out.tokens_generated,
2593 max_tokens,
2594 !c.tool_calls.is_empty() && choice != ToolChoice::None,
2595 )
2596 };
2597 choices.push(Choice {
2598 index: i,
2599 message: message(&c, &choice),
2600 finish: Some(finish),
2601 logprobs: want_logprobs.then(|| decode_logprobs(&daemon, &out.logprobs, 0)),
2602 ..Choice::default()
2603 });
2604 last_finish = finish;
2605 }
2606 access_log(
2607 route,
2608 &model_log,
2609 prompt_tokens,
2610 warm_first.unwrap_or(warm),
2611 completion_tokens,
2612 GenTiming {
2613 t0,
2614 first: ttft_first,
2615 decode,
2616 },
2617 last_finish,
2618 );
2619 Ok::<_, DaemonError>((choices, prompt_tokens, completion_tokens, warm, spec, last_session))
2620 })
2621 .await;
2622 let (choices, prompt_tokens, completion_tokens, warm, spec, session) = out?;
2623 let frame = Frame {
2624 choices,
2625 usage: Some(Usage::complete(
2626 prompt_tokens,
2627 completion_tokens,
2628 Some(warm.min(prompt_tokens)),
2629 )),
2630 spec: (spec.0 > 0).then_some(spec),
2631 session,
2632 ..Frame::default()
2633 };
2634 Ok(Generation {
2635 kind: Kind::Chat,
2636 id,
2637 model: model_name,
2638 created,
2639 warm_header: Some(warm),
2640 body: Body::Complete(frame),
2641 })
2642}
2643
2644async fn chat_stream(
2645 daemon: Arc<Daemon>,
2646 model_name: String,
2647 req: ChatRequest,
2648 policy: RequestPolicy,
2649 explicit_max: Option<u32>,
2650 hold: Option<crate::keypolicy::ClientKey>,
2651) -> Result<Body, RunError> {
2652 let RequestPolicy {
2653 limits,
2654 sampling: op,
2655 qos,
2656 } = policy;
2657 let t0 = std::time::Instant::now();
2658 let admitted_daemon = Arc::clone(&daemon);
2659 let (req, session, seed_drawn, model_defaults) = blocking(qos, hold.clone(), move || {
2660 let model_defaults = admitted_daemon.model_sampling_defaults();
2661 let (session, seed_drawn) = build_session(&admitted_daemon, &req, &op, &model_defaults)?;
2662 qos.apply(&admitted_daemon, session)?;
2663 admitted_daemon.session_fits(session, req.thinking())?;
2664 Ok((req, session, seed_drawn, model_defaults))
2665 })
2666 .await?;
2667 let rx = spawn_stream(qos, hold, move |tx| {
2668 let _closed = crate::keepalive::cancel_when_closed(&daemon, session, tx);
2669 let send = |v: Frame| -> Result<(), DaemonError> {
2670 tx.blocking_send(Ok(v))
2671 .map_err(|_| DaemonError::Protocol("generation consumer gone"))
2672 };
2673 let run = || -> Result<(), DaemonError> {
2674 send(Frame::chat(
2675 Message {
2676 assistant: true,
2677 ..Message::default()
2678 },
2679 None,
2680 ))?;
2681 let mut tool_index = 0usize;
2682 let mut orphaned_calls = 0usize;
2683 let mut extras = extras_of(
2684 req.presence_penalty,
2685 req.frequency_penalty,
2686 req.repeat_penalty,
2687 &op,
2688 &model_defaults,
2689 );
2690 extras.seed_drawn = seed_drawn;
2691 let choice = ToolChoice::parse(&req.tool_choice);
2692 if choice.forces_call() {
2693 extras.open_channel = crate::wal::channel::TOOL_CALL;
2694 }
2695 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
2696 extras.tool_schemas = tool_schemas.clone();
2697 let mut frag =
2698 crate::tool_fragment::ToolCallStream::new(if choice == ToolChoice::None {
2699 crate::tool_fragment::ToolFragmentMode::None
2700 } else {
2701 daemon.tool_fragment_mode()
2702 })
2703 .with_schemas(tool_schemas.clone());
2704 let mut open_call_id = String::new();
2705 let handles = RequestHandles::new(
2706 &daemon,
2707 grammar_handle_for(&daemon, &req, &choice)?,
2708 logit_bias_handle_of(&daemon, &req.logit_bias),
2709 );
2710 handles.apply(&mut extras);
2711 let want_logprobs = req.logprobs.unwrap_or(false);
2712 extras.want_logprobs = want_logprobs;
2713 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
2714 extras.thinking = req.thinking();
2715 extras.ignore_eos = req.ignore_eos;
2716 let lp_q: std::rc::Rc<
2717 std::cell::RefCell<std::collections::VecDeque<crate::scheduler::TokenLogprob>>,
2718 > = std::rc::Rc::new(std::cell::RefCell::new(std::collections::VecDeque::new()));
2719 let lp_fill = std::rc::Rc::clone(&lp_q);
2720 let max_tokens = limits.resolve(explicit_max, || prompt_len(&daemon, session));
2721 let stops = req.stop.list();
2722 let cancels = daemon.cancel_registry();
2723 let delta_mode = !want_logprobs;
2724 let continuous_usage = req
2725 .stream_options
2726 .as_ref()
2727 .is_some_and(|o| o.continuous_usage_stats);
2728 struct StreamText {
2729 usage_now: Option<u32>,
2730 committed: u32,
2731 decode: DecodeSpan,
2732 content_acc: String,
2733 content_sent: usize,
2734 stopped: bool,
2735 dead: bool,
2736 }
2737 let st = std::rc::Rc::new(std::cell::RefCell::new(StreamText {
2738 usage_now: None,
2739 committed: 0,
2740 decode: DecodeSpan::default(),
2741 content_acc: String::new(),
2742 content_sent: 0,
2743 stopped: false,
2744 dead: false,
2745 }));
2746 let push_text =
2747 |s: &mut StreamText, text: &str, lp: Option<Logprobs>| -> Result<(), DaemonError> {
2748 if s.stopped {
2749 return Ok(());
2750 }
2751 if choice.forces_call() {
2752 s.content_acc.push_str(text);
2753 return Ok(());
2754 }
2755 let start = s.content_sent;
2756 s.content_acc.push_str(text);
2757 let tail = &s.content_acc[start..];
2758 let mut cut: Option<usize> = None;
2759 for stop in &stops {
2760 if let Some(i) = tail.find(stop.as_str()) {
2761 cut = Some(cut.map_or(i, |c| c.min(i)));
2762 }
2763 }
2764 let hit = cut.is_some();
2765 let end = match cut {
2766 Some(i) => start + i,
2767 None => start + tail.len() - stop_prefix_holdback(tail, &stops),
2768 };
2769 let piece = s.content_acc.get(start..end).unwrap_or("").to_string();
2770 s.content_sent = s.content_sent.max(end);
2771 if !piece.is_empty() {
2772 let mut ck = Frame::chat(Message::content(piece), None);
2773 if let Some(lp) = lp {
2774 ck.choices[0].logprobs = Some(lp);
2775 }
2776 if let Some(n) = s.usage_now {
2777 ck.usage = Some(Usage::partial(n));
2778 }
2779 send(ck)?;
2780 }
2781 if hit {
2782 cancels.cancel(session);
2783 s.stopped = true;
2784 }
2785 Ok(())
2786 };
2787 let st_delta = std::rc::Rc::clone(&st);
2788 let on_delta = |ch: u32, text: &str, produced: u32| {
2789 if !delta_mode || text.is_empty() {
2790 return;
2791 }
2792 let mut s = st_delta.borrow_mut();
2793 if continuous_usage {
2794 s.usage_now = Some(produced);
2795 }
2796 if s.dead {
2797 return;
2798 }
2799 s.decode.stamp(produced as u64);
2800 let r = if ch == channel::REASONING {
2801 let mut ck = Frame::chat(Message::reasoning(text), None);
2802 if let Some(n) = s.usage_now {
2803 ck.usage = Some(Usage::partial(n));
2804 }
2805 send(ck)
2806 } else {
2807 push_text(&mut s, text, None)
2808 };
2809 if r.is_err() {
2810 cancels.cancel(session);
2811 s.dead = true;
2812 }
2813 };
2814 let st_ev = std::rc::Rc::clone(&st);
2815 let on_event = |ev: &crate::CommittedEvent| {
2816 match &ev.body {
2817 EventBody::Generated {
2818 text,
2819 channel: ch,
2820 span,
2821 ..
2822 } => {
2823 let mut s = st_ev.borrow_mut();
2824 let covered_by_delta = delta_mode
2825 && (*ch == channel::TEXT || *ch == channel::REASONING)
2826 && !text.is_empty();
2827 s.committed += span.len() as u32;
2828 let committed = s.committed as u64;
2829 if !covered_by_delta && !span.is_empty() {
2830 s.decode.stamp(committed);
2831 }
2832 if continuous_usage && !delta_mode {
2833 s.usage_now = Some(s.committed);
2834 }
2835 let these: Vec<crate::scheduler::TokenLogprob> = if want_logprobs {
2836 let mut q = lp_q.borrow_mut();
2837 (0..span.len()).filter_map(|_| q.pop_front()).collect()
2838 } else {
2839 Vec::new()
2840 };
2841 if *ch == channel::TOOL_CALL {
2842 for d in frag.feed(text) {
2843 if d.opening {
2844 open_call_id = format!("call_{}", ev.event_id);
2845 }
2846 let json = Message::tool(&d, tool_index, &open_call_id);
2847 send(Frame::chat(json, None))?;
2848 }
2849 return Ok(());
2850 }
2851 if text.is_empty() {
2852 return Ok(());
2853 }
2854 if delta_mode {
2855 return Ok(());
2856 }
2857 if *ch == channel::REASONING {
2858 let mut ck = Frame::chat(Message::reasoning(text), None);
2859 if let Some(n) = s.usage_now {
2860 ck.usage = Some(Usage::partial(n));
2861 }
2862 return send(ck);
2863 }
2864 let lp = want_logprobs.then(|| decode_logprobs(&daemon, &these, 0));
2865 push_text(&mut s, text, lp)
2866 }
2867 EventBody::ToolParseFailure { raw } => {
2868 if choice != ToolChoice::None && !frag.opened() {
2869 if let Some((name, arguments)) = daemon.named_tool_call(raw) {
2870 let delta = Message::complete_tool(
2871 tool_index,
2872 format!("call_{}", ev.event_id),
2873 name,
2874 arguments,
2875 );
2876 tool_index += 1;
2877 orphaned_calls += 1;
2878 return send(Frame::chat(delta, None));
2879 }
2880 }
2881 if frag.opened() {
2882 orphaned_calls += 1;
2883 tool_index += 1;
2884 }
2885 frag.reset();
2886 let mut ck = Frame::chat(Message::content(raw), None);
2887 if continuous_usage {
2888 ck.usage = Some(Usage::partial(st_ev.borrow().committed));
2889 }
2890 send(ck)
2891 }
2892 EventBody::ToolUse { name, arguments } => {
2893 if choice == ToolChoice::None {
2894 let raw = format!(
2895 "{{\"name\":{},\"arguments\":{}}}",
2896 serde_json::Value::String(name.clone()),
2897 arguments
2898 );
2899 let mut ck = Frame::chat(Message::content(raw), None);
2900 if continuous_usage {
2901 ck.usage = Some(Usage::partial(st_ev.borrow().committed));
2902 }
2903 return send(ck);
2904 }
2905 if frag.opened() {
2906 for d in frag.finish(arguments) {
2907 send(Frame::chat(
2908 Message::tool(&d, tool_index, &open_call_id),
2909 None,
2910 ))?;
2911 }
2912 frag.reset();
2913 tool_index += 1;
2914 return Ok(());
2915 }
2916 let delta = Message::complete_tool(
2917 tool_index,
2918 format!("call_{}", ev.event_id),
2919 name.clone(),
2920 arguments.clone(),
2921 );
2922 tool_index += 1;
2923 send(Frame::chat(delta, None))
2924 }
2925 _ => Ok(()),
2926 }
2927 };
2928 let on_logprobs = |items: &[crate::scheduler::TokenLogprob]| {
2929 lp_fill.borrow_mut().extend(items.iter().cloned());
2930 };
2931 let out = if delta_mode {
2932 daemon.generate_streaming_delta(
2933 session,
2934 max_tokens,
2935 extras,
2936 on_event,
2937 on_logprobs,
2938 on_delta,
2939 )?
2940 } else {
2941 daemon.generate_streaming_ex_lp(
2942 session,
2943 max_tokens,
2944 extras,
2945 on_event,
2946 on_logprobs,
2947 )?
2948 };
2949 let st = st.borrow();
2950 let (mut decode, stopped) = (st.decode, st.stopped);
2951 let first_tok = decode.first_token();
2952 decode.finish();
2953 decode.add_measured(out.decode);
2954 let (content_acc, content_sent) = (st.content_acc.clone(), st.content_sent);
2955 drop(st);
2956 let mut c = collect(&out.events);
2957 if !stopped && !choice.forces_call() && content_sent < content_acc.len() {
2958 let rest = content_acc[content_sent..].to_string();
2959 let mut ck = Frame::chat(Message::content(rest), None);
2960 if continuous_usage {
2961 ck.usage = Some(Usage::partial(out.tokens_generated));
2962 }
2963 send(ck)?;
2964 }
2965 if choice.forces_call() {
2966 c.content = content_acc.clone();
2967 promote_forced_tool_call(&daemon, &mut c, session, tool_schemas.as_deref());
2968 if tool_index > 0 {
2969 } else if let Some((id, name, args)) = c.tool_calls.first() {
2970 send(Frame::chat(
2971 Message::complete_tool(
2972 tool_index,
2973 format!("call_{id}"),
2974 name.clone(),
2975 args.clone(),
2976 ),
2977 None,
2978 ))?;
2979 } else if !c.content.is_empty() {
2980 let mut ck = Frame::chat(Message::content(c.content), None);
2981 if continuous_usage {
2982 ck.usage = Some(Usage::partial(out.tokens_generated));
2983 }
2984 send(ck)?;
2985 }
2986 }
2987 let fin = if stopped {
2988 "stop"
2989 } else {
2990 finish_reason(
2991 out.finish,
2992 out.tokens_generated,
2993 max_tokens,
2994 !c.tool_calls.is_empty() && choice != ToolChoice::None,
2995 )
2996 };
2997 let fin = if orphaned_calls > 0 { "length" } else { fin };
2998 let mut terminal = Frame::chat(Message::default(), Some(fin));
2999 terminal.usage = Some(usage(
3000 &daemon,
3001 session,
3002 out.tokens_generated,
3003 out.warm_prefix,
3004 ));
3005 access_log(
3006 &format!("{} (stream)", req.route()),
3007 &model_name,
3008 terminal.usage.as_ref().and_then(|u| u.prompt).unwrap_or(0),
3009 out.warm_prefix,
3010 out.tokens_generated as u64,
3011 GenTiming {
3012 t0,
3013 first: first_tok,
3014 decode,
3015 },
3016 fin,
3017 );
3018 terminal.warm = Some(out.warm_prefix);
3019 terminal.spec = (out.spec.0 > 0).then_some(out.spec);
3020 terminal.session = Some(session);
3021 send(terminal)
3022 };
3023 run()
3024 });
3025
3026 Ok(Body::Stream(rx))
3027}
3028const FIM_DEFAULT_MAX_TOKENS: u32 = 128;
3029
3030#[allow(clippy::too_many_arguments)]
3031pub(crate) async fn fim_completions(
3032 daemon: Arc<Daemon>,
3033 model_name: String,
3034 req: CompletionRequest,
3035 limits: TokenLimits,
3036 sampling: SamplingOverrides,
3037 mode: u8,
3038 key: Option<crate::keypolicy::ClientKey>,
3039) -> Result<Generation, RunError> {
3040 if !daemon.fim_supported() {
3041 return Err(DaemonError::NoFimDialect.into());
3042 }
3043 let hold = key;
3044 let max_tokens = req
3045 .max_tokens
3046 .or(limits.default_max_tokens)
3047 .unwrap_or(FIM_DEFAULT_MAX_TOKENS)
3048 .max(1);
3049 let serving = daemon
3050 .fim_satellite()
3051 .unwrap_or_else(|| Arc::clone(&daemon));
3052 let model_defaults = {
3053 let d = Arc::clone(&serving);
3054 tokio::task::spawn_blocking(move || {
3055 crate::with_sync_class(Some(crate::qos::INLINE_COMPLETION), || {
3056 d.model_sampling_defaults()
3057 })
3058 })
3059 .await
3060 .unwrap_or_default()
3061 };
3062 let op = sampling.for_daemon(&serving);
3063 let mut params = params_of_for(
3064 req.temperature,
3065 req.top_p,
3066 req.top_k,
3067 req.min_p,
3068 req.seed,
3069 &model_defaults,
3070 &op,
3071 );
3072 let seed_drawn = draw_seed_if_unseeded(&mut params, req.seed);
3073 let mut extras = extras_of(
3074 req.presence_penalty,
3075 req.frequency_penalty,
3076 req.repeat_penalty,
3077 &op,
3078 &model_defaults,
3079 );
3080 extras.seed_drawn = seed_drawn;
3081 extras.ignore_eos = req.ignore_eos;
3082 let fim = crate::FimRequest {
3083 prefix: req.prompt,
3084 suffix: req.suffix.unwrap_or_default(),
3085 mode,
3086 max_tokens,
3087 params,
3088 extras,
3089 };
3090 let id = format!(
3091 "cmpl-superfluid-{}",
3092 next_id().rsplit('-').next().unwrap_or("0")
3093 );
3094 let created = unix_now();
3095 let served_by = daemon
3096 .fim_satellite_name()
3097 .unwrap_or_else(|| model_name.clone());
3098 let log_model = served_by.clone();
3099 let fin_of = move |c: &crate::Completion| {
3100 if c.expired {
3101 "length"
3102 } else {
3103 finish_reason(c.finish, c.tokens.len() as u32, max_tokens, false)
3104 }
3105 };
3106 let ext = move |c: &crate::Completion| FimMetadata {
3107 cached: c.cached,
3108 expired: c.expired,
3109 served_by: served_by.clone(),
3110 };
3111 if req.stream {
3112 let admitted_daemon = Arc::clone(&daemon);
3113 let fim = blocking(
3114 RequestQos {
3115 class: crate::qos::INLINE_COMPLETION,
3116 batch_invariant: false,
3117 },
3118 hold.clone(),
3119 move || {
3120 admitted_daemon.fim_fits(&fim)?;
3121 Ok(fim)
3122 },
3123 )
3124 .await?;
3125 let (tx, rx) = tokio::sync::mpsc::channel(64);
3126 tokio::task::spawn_blocking(move || {
3127 let _hold = hold;
3128 let t0 = std::time::Instant::now();
3129 let mut decode = DecodeSpan::default();
3130 let mut delivered = 0usize;
3131 let res = daemon.complete_streaming(&fim, |piece, produced| {
3132 decode.stamp(produced as u64);
3133 delivered = delivered.max(produced);
3134 match tx.blocking_send(Ok(Frame::text(piece, None, None))) {
3135 Ok(()) => std::ops::ControlFlow::Continue(()),
3136 Err(_) => std::ops::ControlFlow::Break(()),
3137 }
3138 });
3139 match res {
3140 Ok(c) => {
3141 let fin = fin_of(&c);
3142 let first = decode.first_token();
3143 decode.finish();
3144 access_log(
3145 "POST /v1/completions (fim)",
3146 &log_model,
3147 c.prompt_tokens as u64,
3148 fim_log_cached(&c),
3149 c.tokens.len().max(delivered) as u64,
3150 GenTiming { t0, first, decode },
3151 fin,
3152 );
3153 let mut last = Frame::text("", None, Some(fin));
3154 last.fim = Some(ext(&c));
3155 last.usage = Some(Usage::complete(
3156 c.prompt_tokens as u64,
3157 c.tokens.len() as u64,
3158 None,
3159 ));
3160 let _ = tx.blocking_send(Ok(last));
3161 }
3162 Err(e) => {
3163 let client_error = matches!(
3164 e,
3165 DaemonError::StreamTooLong { .. } | DaemonError::NoFimDialect
3166 );
3167 let _ = tx.blocking_send(Err(StreamFailure::new(e, client_error)));
3168 }
3169 }
3170 });
3171 return Ok(Generation {
3172 kind: Kind::Completion,
3173 id,
3174 model: model_name,
3175 created,
3176 warm_header: None,
3177 body: Body::Stream(rx),
3178 });
3179 }
3180 let out = tokio::task::spawn_blocking(move || {
3181 let _hold = hold;
3182 let t0 = std::time::Instant::now();
3183 let mut decode = DecodeSpan::default();
3184 let c = daemon.complete_streaming(&fim, |_, produced| {
3185 decode.stamp(produced as u64);
3186 std::ops::ControlFlow::Continue(())
3187 })?;
3188 let first = decode.first_token();
3189 decode.finish();
3190 access_log(
3191 "POST /v1/completions (fim)",
3192 &log_model,
3193 c.prompt_tokens as u64,
3194 fim_log_cached(&c),
3195 c.tokens.len() as u64,
3196 GenTiming { t0, first, decode },
3197 fin_of(&c),
3198 );
3199 Ok::<_, DaemonError>(c)
3200 })
3201 .await;
3202 let c = out.map_err(|e| RunError::Worker(e.to_string()))??;
3203 let mut frame = Frame::text(&c.text, None, Some(fin_of(&c)));
3204 frame.usage = Some(Usage::complete(
3205 c.prompt_tokens as u64,
3206 c.tokens.len() as u64,
3207 None,
3208 ));
3209 frame.fim = Some(ext(&c));
3210 Ok(Generation {
3211 kind: Kind::Completion,
3212 id,
3213 model: model_name,
3214 created,
3215 warm_header: None,
3216 body: Body::Complete(frame),
3217 })
3218}
3219
3220pub(crate) fn chat_once(
3221 daemon: &Daemon,
3222 req: &ChatRequest,
3223 policy: RequestPolicy,
3224) -> Result<Frame, DaemonError> {
3225 let RequestPolicy {
3226 limits,
3227 sampling: op,
3228 qos,
3229 } = policy;
3230 let _class = crate::SyncClassScope::enter(Some(qos.class));
3231 let model_defaults = daemon.model_sampling_defaults();
3232 let mut extras = extras_of(
3233 req.presence_penalty,
3234 req.frequency_penalty,
3235 req.repeat_penalty,
3236 &op,
3237 &model_defaults,
3238 );
3239 let choice = ToolChoice::parse(&req.tool_choice);
3240 if choice.forces_call() {
3241 extras.open_channel = crate::wal::channel::TOOL_CALL;
3242 }
3243 let tool_schemas = crate::codec::ToolSchemas::of_values(&req.tools);
3244 extras.tool_schemas = tool_schemas.clone();
3245 let handles = RequestHandles::new(
3246 daemon,
3247 grammar_handle_for(daemon, req, &choice)?,
3248 logit_bias_handle_of(daemon, &req.logit_bias),
3249 );
3250 handles.apply(&mut extras);
3251 let want_logprobs = req.logprobs.unwrap_or(false);
3252 extras.want_logprobs = want_logprobs;
3253 extras.top_logprobs = req.top_logprobs.unwrap_or(0).min(20) as u8;
3254 extras.thinking = req.thinking();
3255 extras.ignore_eos = req.ignore_eos;
3256 let stops = req.stop.list();
3257 let (session, seed_drawn) = build_session(daemon, req, &op, &model_defaults)?;
3258 extras.seed_drawn = seed_drawn;
3259 qos.apply(daemon, session)?;
3260 let max_tokens = limits.resolve(req.max_completion_tokens.or(req.max_tokens), || {
3261 prompt_len(daemon, session)
3262 });
3263 let cancels = daemon.cancel_registry();
3264 let mut acc = String::new();
3265 let mut stopped = false;
3266 let out = daemon.generate_streaming_ex(session, max_tokens, extras, |ev| {
3267 if let EventBody::Generated {
3268 text, channel: ch, ..
3269 } = &ev.body
3270 {
3271 if *ch == channel::TEXT && !stops.is_empty() && !stopped {
3272 acc.push_str(text);
3273 if stops.iter().any(|s| acc.contains(s.as_str())) {
3274 cancels.cancel(session);
3275 stopped = true;
3276 }
3277 }
3278 }
3279 Ok(())
3280 });
3281 let out = out?;
3282 let mut c = collect(&out.events);
3283 claim_named_calls(daemon, &mut c, &out.events, &choice);
3284 if choice.forces_call() {
3285 promote_forced_tool_call(daemon, &mut c, session, tool_schemas.as_deref());
3286 }
3287 let (content, hit_stop) = truncate_at_stop(&c.content, &stops);
3288 c.content = content;
3289 let usage = usage(daemon, session, out.tokens_generated, out.warm_prefix);
3290 let finish = if !c.broken_calls.is_empty() {
3291 "length"
3292 } else if hit_stop {
3293 "stop"
3294 } else {
3295 finish_reason(
3296 out.finish,
3297 out.tokens_generated,
3298 max_tokens,
3299 !c.tool_calls.is_empty() && choice != ToolChoice::None,
3300 )
3301 };
3302 Ok(Frame {
3303 choices: vec![Choice {
3304 message: message(&c, &choice),
3305 finish: Some(finish),
3306 logprobs: want_logprobs.then(|| decode_logprobs(daemon, &out.logprobs, 0)),
3307 ..Choice::default()
3308 }],
3309 usage: Some(usage),
3310 spec: (out.spec.0 > 0).then_some(out.spec),
3311 ..Frame::default()
3312 })
3313}
3314
3315pub(crate) async fn blocking<T: Send + 'static>(
3316 qos: RequestQos,
3317 hold: Option<crate::keypolicy::ClientKey>,
3318 work: impl FnOnce() -> Result<T, DaemonError> + Send + 'static,
3319) -> Result<T, RunError> {
3320 tokio::task::spawn_blocking(move || {
3321 let _hold = hold;
3322 let _class = crate::SyncClassScope::enter(Some(qos.class));
3323 work()
3324 })
3325 .await
3326 .map_err(|e| RunError::Worker(e.to_string()))?
3327 .map_err(RunError::Daemon)
3328}
3329
3330pub(crate) fn base64_decode(data: &str) -> Result<Vec<u8>, DaemonError> {
3331 use base64::Engine;
3332 let cleaned: String = data.chars().filter(|c| !c.is_whitespace()).collect();
3333 base64::engine::general_purpose::STANDARD
3334 .decode(&cleaned)
3335 .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(&cleaned))
3336 .map_err(|_| DaemonError::Protocol("invalid base64 image data"))
3337}
3338
3339pub(crate) fn spawn_stream<T: Send + 'static>(
3340 qos: RequestQos,
3341 hold: Option<crate::keypolicy::ClientKey>,
3342 work: impl FnOnce(&tokio::sync::mpsc::Sender<Result<T, StreamFailure>>) -> Result<(), DaemonError>
3343 + Send
3344 + 'static,
3345) -> tokio::sync::mpsc::Receiver<Result<T, StreamFailure>> {
3346 let (tx, rx) = tokio::sync::mpsc::channel(64);
3347 tokio::task::spawn_blocking(move || {
3348 let _hold = hold;
3349 let _class = crate::SyncClassScope::enter(Some(qos.class));
3350 if let Err(error) = work(&tx) {
3351 let client_error = matches!(
3352 error,
3353 DaemonError::Constraint(_)
3354 | DaemonError::TemplateRefused(_)
3355 | DaemonError::Unsupported(_)
3356 | DaemonError::StreamTooLong { .. }
3357 | DaemonError::NoFimDialect
3358 );
3359 let _ = tx.blocking_send(Err(StreamFailure::new(error, client_error)));
3360 }
3361 });
3362 rx
3363}