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