1use std::ffi::{c_char, CString};
24use std::ptr::NonNull;
25
26use llama_cpp_sys_4 as sys;
27
28use crate::context::LlamaContext;
29use crate::model::LlamaModel;
30use crate::token::LlamaToken;
31
32pub type CommonSamplerError = crate::shim::ShimError;
38
39use crate::shim::{check_status, last_error, read_i32s, read_string, read_tokens, Result};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[repr(i32)]
50pub enum CommonSamplerType {
51 Dry = 1,
53 TopK = 2,
55 TopP = 3,
57 MinP = 4,
59 TypicalP = 6,
61 Temperature = 7,
63 Xtc = 8,
65 Infill = 9,
67 Penalties = 10,
69 TopNSigma = 11,
71 AdaptiveP = 12,
73}
74
75impl CommonSamplerType {
76 pub fn name(self) -> Result<String> {
82 read_string(|buf, len, expected| unsafe {
83 sys::common_shim_sampler_type_to_str(self as i32, buf, len, expected)
84 })
85 }
86
87 pub fn from_names(names: &[&str]) -> Result<Vec<i32>> {
96 let c_names: Vec<CString> = names
97 .iter()
98 .map(|n| CString::new(*n))
99 .collect::<std::result::Result<_, _>>()?;
100 let ptrs: Vec<*const c_char> = c_names.iter().map(|c| c.as_ptr()).collect();
101
102 read_i32s(|out, cap, len| unsafe {
103 sys::common_shim_sampler_types_from_names(ptrs.as_ptr(), ptrs.len(), out, cap, len)
104 })
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum GrammarSource {
116 #[default]
118 None,
119 User,
121 OutputFormat,
123 ToolCalls,
125}
126
127impl GrammarSource {
128 #[allow(clippy::cast_possible_wrap)]
130 fn as_raw(self) -> i32 {
131 let raw = match self {
132 Self::None => sys::COMMON_SHIM_GRAMMAR_NONE,
133 Self::User => sys::COMMON_SHIM_GRAMMAR_USER,
134 Self::OutputFormat => sys::COMMON_SHIM_GRAMMAR_OUTPUT_FORMAT,
135 Self::ToolCalls => sys::COMMON_SHIM_GRAMMAR_TOOL_CALLS,
136 };
137 raw as i32
138 }
139}
140
141pub struct CommonSamplerParams {
147 raw: NonNull<sys::common_shim_sampler_params>,
148}
149
150impl std::fmt::Debug for CommonSamplerParams {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("CommonSamplerParams")
153 .field("scalars", &self.scalars())
154 .finish_non_exhaustive()
155 }
156}
157
158unsafe impl Send for CommonSamplerParams {}
161
162impl Drop for CommonSamplerParams {
163 fn drop(&mut self) {
164 unsafe { sys::common_shim_sampler_params_free(self.raw.as_ptr()) }
165 }
166}
167
168pub type CommonSamplerScalars = sys::common_shim_sampler_scalars;
170
171impl Default for CommonSamplerParams {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl CommonSamplerParams {
178 #[must_use]
184 pub fn new() -> Self {
185 let raw = unsafe { sys::common_shim_sampler_params_init() };
186 Self {
187 raw: NonNull::new(raw).expect("common_shim_sampler_params_init returned null"),
188 }
189 }
190
191 #[must_use]
193 pub fn scalars(&self) -> CommonSamplerScalars {
194 let mut out: CommonSamplerScalars = unsafe { std::mem::zeroed() };
195 unsafe { sys::common_shim_sampler_params_get_scalars(self.raw.as_ptr(), &raw mut out) };
196 out
197 }
198
199 pub fn set_scalars(&mut self, scalars: &CommonSamplerScalars) {
202 unsafe { sys::common_shim_sampler_params_set_scalars(self.raw.as_ptr(), scalars) }
203 }
204
205 pub fn set_grammar(&mut self, grammar: &str, source: GrammarSource, lazy: bool) -> Result<()> {
214 let c_grammar = CString::new(grammar)?;
215 let status = unsafe {
216 sys::common_shim_sampler_params_set_grammar(
217 self.raw.as_ptr(),
218 c_grammar.as_ptr(),
219 source.as_raw(),
220 lazy,
221 )
222 };
223 check_status(status)
224 }
225
226 pub fn add_grammar_trigger(
236 &mut self,
237 kind: &str,
238 value: &str,
239 token: LlamaToken,
240 ) -> Result<()> {
241 let raw_kind = match kind {
242 "token" => 0,
243 "word" => 1,
244 "pattern" => 2,
245 "pattern_full" => 3,
246 _ => return Err(CommonSamplerError::InvalidArg),
247 };
248 let c_value = CString::new(value)?;
249 let status = unsafe {
250 sys::common_shim_sampler_params_add_grammar_trigger(
251 self.raw.as_ptr(),
252 raw_kind,
253 c_value.as_ptr(),
254 token.0,
255 )
256 };
257 check_status(status)
258 }
259
260 pub fn set_generation_prompt(&mut self, prompt: &str) -> Result<()> {
266 let c_prompt = CString::new(prompt)?;
267 let status = unsafe {
268 sys::common_shim_sampler_params_set_generation_prompt(
269 self.raw.as_ptr(),
270 c_prompt.as_ptr(),
271 )
272 };
273 check_status(status)
274 }
275
276 pub fn add_logit_bias(&mut self, token: LlamaToken, bias: f32) -> Result<()> {
282 let status = unsafe {
283 sys::common_shim_sampler_params_add_logit_bias(self.raw.as_ptr(), token.0, bias)
284 };
285 check_status(status)
286 }
287
288 pub fn set_samplers(&mut self, samplers: &[CommonSamplerType]) -> Result<()> {
294 let raw: Vec<i32> = samplers.iter().map(|s| *s as i32).collect();
295 let status = unsafe {
296 sys::common_shim_sampler_params_set_samplers(self.raw.as_ptr(), raw.as_ptr(), raw.len())
297 };
298 check_status(status)
299 }
300
301 pub fn set_dry_breakers(&mut self, breakers: &[&str]) -> Result<()> {
307 let c_breakers: Vec<CString> = breakers
308 .iter()
309 .map(|b| CString::new(*b))
310 .collect::<std::result::Result<_, _>>()?;
311 let ptrs: Vec<*const c_char> = c_breakers.iter().map(|c| c.as_ptr()).collect();
312 let status = unsafe {
313 sys::common_shim_sampler_params_set_dry_breakers(
314 self.raw.as_ptr(),
315 ptrs.as_ptr(),
316 ptrs.len(),
317 )
318 };
319 check_status(status)
320 }
321
322 pub fn set_reasoning_budget(
334 &mut self,
335 start: &[LlamaToken],
336 ends: &[Vec<LlamaToken>],
337 forced: &[LlamaToken],
338 message: &str,
339 ) -> Result<()> {
340 let start_raw: Vec<i32> = start.iter().map(|t| t.0).collect();
341 let (ends_flat, end_lens) = flatten(ends);
342 let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
343 let c_message = CString::new(message)?;
344
345 let status = unsafe {
346 sys::common_shim_sampler_params_set_reasoning_budget(
347 self.raw.as_ptr(),
348 start_raw.as_ptr(),
349 start_raw.len(),
350 ends_flat.as_ptr(),
351 end_lens.as_ptr(),
352 end_lens.len(),
353 forced_raw.as_ptr(),
354 forced_raw.len(),
355 c_message.as_ptr(),
356 )
357 };
358 check_status(status)
359 }
360}
361
362fn flatten(seqs: &[Vec<LlamaToken>]) -> (Vec<i32>, Vec<usize>) {
365 let mut data = Vec::new();
366 let mut lens = Vec::with_capacity(seqs.len());
367 for seq in seqs {
368 lens.push(seq.len());
369 data.extend(seq.iter().map(|t| t.0));
370 }
371 (data, lens)
372}
373
374pub struct CommonSampler {
376 raw: NonNull<sys::common_shim_sampler>,
377}
378
379impl std::fmt::Debug for CommonSampler {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 f.debug_struct("CommonSampler")
382 .field("seed", &self.seed())
383 .finish_non_exhaustive()
384 }
385}
386
387unsafe impl Send for CommonSampler {}
390
391impl Drop for CommonSampler {
392 fn drop(&mut self) {
393 unsafe { sys::common_shim_sampler_free(self.raw.as_ptr()) }
394 }
395}
396
397impl CommonSampler {
398 pub fn new(model: &LlamaModel, params: &mut CommonSamplerParams) -> Result<Self> {
405 let raw =
406 unsafe { sys::common_shim_sampler_init(model.model.as_ptr(), params.raw.as_ptr()) };
407 NonNull::new(raw)
408 .map(|raw| Self { raw })
409 .ok_or_else(|| CommonSamplerError::Failed(last_error()))
410 }
411
412 pub fn sample(
423 &mut self,
424 ctx: &mut LlamaContext<'_>,
425 idx: i32,
426 grammar_first: bool,
427 ) -> Result<LlamaToken> {
428 let mut status = sys::LLAMA_SHIM_OK;
429 let token = unsafe {
430 sys::common_shim_sampler_sample(
431 self.raw.as_ptr(),
432 ctx.context.as_ptr(),
433 idx,
434 grammar_first,
435 &raw mut status,
436 )
437 };
438 check_status(status)?;
439 Ok(LlamaToken(token))
440 }
441
442 pub fn accept(&mut self, token: LlamaToken, is_generated: bool) {
447 unsafe { sys::common_shim_sampler_accept(self.raw.as_ptr(), token.0, is_generated) }
448 }
449
450 pub fn sample_and_accept_n(
465 &mut self,
466 ctx: &mut LlamaContext<'_>,
467 draft: &[LlamaToken],
468 grammar_first: bool,
469 ) -> Result<Vec<LlamaToken>> {
470 let raw_draft: Vec<i32> = draft.iter().map(|t| t.0).collect();
471 read_tokens(|out, cap, len| unsafe {
472 sys::common_shim_sampler_sample_and_accept_n(
473 self.raw.as_ptr(),
474 ctx.context.as_ptr(),
475 raw_draft.as_ptr(),
476 raw_draft.len(),
477 grammar_first,
478 out,
479 cap,
480 len,
481 )
482 })
483 }
484
485 pub fn reset(&mut self) {
487 unsafe { sys::common_shim_sampler_reset(self.raw.as_ptr()) }
488 }
489
490 pub fn try_clone(&self) -> Result<Self> {
496 let raw = unsafe { sys::common_shim_sampler_clone(self.raw.as_ptr()) };
497 NonNull::new(raw)
498 .map(|raw| Self { raw })
499 .ok_or_else(|| CommonSamplerError::Failed(last_error()))
500 }
501
502 #[must_use]
504 pub fn seed(&self) -> u32 {
505 unsafe { sys::common_shim_sampler_get_seed(self.raw.as_ptr()) }
506 }
507
508 #[must_use]
510 pub fn last(&self) -> LlamaToken {
511 LlamaToken(unsafe { sys::common_shim_sampler_last(self.raw.as_ptr()) })
512 }
513
514 pub fn force_end_reasoning(&mut self) -> bool {
518 unsafe { sys::common_shim_sampler_reasoning_budget_force(self.raw.as_ptr()) }
519 }
520
521 pub fn describe(&self) -> Result<String> {
527 read_string(|buf, len, expected| unsafe {
528 sys::common_shim_sampler_print(self.raw.as_ptr(), buf, len, expected)
529 })
530 }
531
532 pub fn prev_str(&mut self, ctx: &mut LlamaContext<'_>, n: i32) -> Result<String> {
538 read_string(|buf, len, expected| unsafe {
539 sys::common_shim_sampler_prev_str(
540 self.raw.as_ptr(),
541 ctx.context.as_ptr(),
542 n,
543 buf,
544 len,
545 expected,
546 )
547 })
548 }
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557pub enum ReasoningBudgetState {
558 Idle,
560 Counting,
562 Forcing,
564 WaitingUtf8,
566 Done,
568}
569
570impl ReasoningBudgetState {
571 #[allow(clippy::cast_possible_wrap)]
573 fn from_raw(raw: i32) -> Option<Self> {
574 if raw == sys::COMMON_SHIM_RBUDGET_IDLE as i32 {
575 Some(Self::Idle)
576 } else if raw == sys::COMMON_SHIM_RBUDGET_COUNTING as i32 {
577 Some(Self::Counting)
578 } else if raw == sys::COMMON_SHIM_RBUDGET_FORCING as i32 {
579 Some(Self::Forcing)
580 } else if raw == sys::COMMON_SHIM_RBUDGET_WAITING_UTF8 as i32 {
581 Some(Self::WaitingUtf8)
582 } else if raw == sys::COMMON_SHIM_RBUDGET_DONE as i32 {
583 Some(Self::Done)
584 } else {
585 None
586 }
587 }
588
589 #[allow(clippy::cast_possible_wrap)]
590 fn as_raw(self) -> i32 {
591 let raw = match self {
592 Self::Idle => sys::COMMON_SHIM_RBUDGET_IDLE,
593 Self::Counting => sys::COMMON_SHIM_RBUDGET_COUNTING,
594 Self::Forcing => sys::COMMON_SHIM_RBUDGET_FORCING,
595 Self::WaitingUtf8 => sys::COMMON_SHIM_RBUDGET_WAITING_UTF8,
596 Self::Done => sys::COMMON_SHIM_RBUDGET_DONE,
597 };
598 raw as i32
599 }
600}
601
602#[derive(Debug)]
617pub struct ReasoningBudget {
618 sampler: crate::sampling::LlamaSampler,
619}
620
621impl ReasoningBudget {
622 pub fn new(
634 model: &LlamaModel,
635 starts: &[Vec<LlamaToken>],
636 ends: &[Vec<LlamaToken>],
637 forced: &[LlamaToken],
638 budget: i32,
639 ) -> Result<Self> {
640 Self::with_initial_state(
641 model,
642 starts,
643 ends,
644 forced,
645 budget,
646 ReasoningBudgetState::Idle,
647 )
648 }
649
650 pub fn with_initial_state(
659 model: &LlamaModel,
660 starts: &[Vec<LlamaToken>],
661 ends: &[Vec<LlamaToken>],
662 forced: &[LlamaToken],
663 budget: i32,
664 initial_state: ReasoningBudgetState,
665 ) -> Result<Self> {
666 let (starts_flat, start_lens) = flatten(starts);
667 let (ends_flat, end_lens) = flatten(ends);
668 let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
669
670 let raw = unsafe {
671 sys::common_shim_reasoning_budget_init(
672 model.get_vocab().vocab.as_ref(),
673 starts_flat.as_ptr(),
674 start_lens.as_ptr(),
675 start_lens.len(),
676 ends_flat.as_ptr(),
677 end_lens.as_ptr(),
678 end_lens.len(),
679 forced_raw.as_ptr(),
680 forced_raw.len(),
681 budget,
682 initial_state.as_raw(),
683 )
684 };
685 let ptr = NonNull::new(raw).ok_or_else(|| CommonSamplerError::Failed(last_error()))?;
686 Ok(Self {
689 sampler: unsafe { crate::sampling::LlamaSampler::from_raw_ptr(ptr) },
690 })
691 }
692
693 #[must_use]
695 pub fn state(&self) -> Option<ReasoningBudgetState> {
696 let raw = unsafe {
697 sys::common_shim_reasoning_budget_get_state(self.sampler.as_ptr().cast_const())
698 };
699 ReasoningBudgetState::from_raw(raw)
700 }
701
702 pub fn force_end(&mut self) -> bool {
705 unsafe { sys::common_shim_reasoning_budget_force(self.sampler.as_ptr()) }
706 }
707
708 #[must_use]
710 pub fn into_sampler(self) -> crate::sampling::LlamaSampler {
711 self.sampler
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn params_start_from_llama_cpp_defaults() {
721 let params = CommonSamplerParams::new();
722 let s = params.scalars();
723 assert!((s.temp - 0.80).abs() < 1e-6, "temp = {}", s.temp);
726 assert_eq!(s.top_k, 40);
727 assert!((s.top_p - 0.95).abs() < 1e-6, "top_p = {}", s.top_p);
728 assert!((s.min_p - 0.05).abs() < 1e-6, "min_p = {}", s.min_p);
729 assert_eq!(s.mirostat, 0);
730 assert_eq!(s.reasoning_budget_tokens, -1, "budget disabled by default");
731 }
732
733 #[test]
734 fn scalars_round_trip() {
735 let mut params = CommonSamplerParams::new();
736 let mut s = params.scalars();
737 s.temp = 0.25;
738 s.top_k = 7;
739 s.reasoning_budget_tokens = 128;
740 params.set_scalars(&s);
741
742 let back = params.scalars();
743 assert!((back.temp - 0.25).abs() < 1e-6);
744 assert_eq!(back.top_k, 7);
745 assert_eq!(back.reasoning_budget_tokens, 128);
746 }
747
748 #[test]
751 fn setting_scalars_preserves_untouched_fields() {
752 let mut params = CommonSamplerParams::new();
753 let before = params.scalars();
754 let mut s = before;
755 s.top_k = 3;
756 params.set_scalars(&s);
757 let after = params.scalars();
758
759 assert_eq!(after.top_k, 3);
760 assert!((after.top_p - before.top_p).abs() < 1e-6);
761 assert!((after.dry_base - before.dry_base).abs() < 1e-6);
762 assert_eq!(after.penalty_last_n, before.penalty_last_n);
763 assert_eq!(after.seed, before.seed);
764 }
765
766 #[test]
767 fn sampler_type_names_match_upstream() {
768 assert_eq!(CommonSamplerType::TopK.name().unwrap(), "top_k");
769 assert_eq!(CommonSamplerType::TopP.name().unwrap(), "top_p");
770 assert_eq!(
771 CommonSamplerType::Temperature.name().unwrap(),
772 "temperature"
773 );
774 }
775
776 #[test]
779 fn sampler_type_discriminants_round_trip_through_names() {
780 for ty in [
781 CommonSamplerType::Dry,
782 CommonSamplerType::TopK,
783 CommonSamplerType::TopP,
784 CommonSamplerType::MinP,
785 CommonSamplerType::TypicalP,
786 CommonSamplerType::Temperature,
787 CommonSamplerType::Xtc,
788 CommonSamplerType::Infill,
789 CommonSamplerType::Penalties,
790 CommonSamplerType::TopNSigma,
791 CommonSamplerType::AdaptiveP,
792 ] {
793 let name = ty.name().unwrap();
794 let parsed = CommonSamplerType::from_names(&[&name]).unwrap();
795 assert_eq!(parsed, vec![ty as i32], "{name} did not round-trip");
796 }
797 }
798
799 #[test]
800 fn unknown_sampler_names_are_dropped() {
801 let parsed = CommonSamplerType::from_names(&["top_k", "not_a_sampler"]).unwrap();
802 assert_eq!(parsed, vec![CommonSamplerType::TopK as i32]);
803 }
804
805 #[test]
806 fn grammar_trigger_rejects_unknown_kind() {
807 let mut params = CommonSamplerParams::new();
808 assert!(matches!(
809 params.add_grammar_trigger("nonsense", "x", LlamaToken(-1)),
810 Err(CommonSamplerError::InvalidArg)
811 ));
812 }
813
814 #[test]
815 fn grammar_trigger_accepts_every_known_kind() {
816 let mut params = CommonSamplerParams::new();
817 for kind in ["token", "word", "pattern", "pattern_full"] {
818 params
819 .add_grammar_trigger(kind, "<tool_call>", LlamaToken(1))
820 .unwrap_or_else(|e| panic!("{kind} rejected: {e}"));
821 }
822 }
823
824 #[test]
825 fn interior_nul_is_rejected_not_truncated() {
826 let mut params = CommonSamplerParams::new();
827 assert!(matches!(
828 params.set_grammar("root ::= \0 \"a\"", GrammarSource::User, false),
829 Err(CommonSamplerError::Nul(_))
830 ));
831 assert!(matches!(
832 params.set_generation_prompt("a\0b"),
833 Err(CommonSamplerError::Nul(_))
834 ));
835 }
836
837 #[test]
838 fn flatten_produces_matching_data_and_lengths() {
839 let seqs = vec![
840 vec![LlamaToken(1), LlamaToken(2)],
841 vec![],
842 vec![LlamaToken(3)],
843 ];
844 let (data, lens) = flatten(&seqs);
845 assert_eq!(data, vec![1, 2, 3]);
846 assert_eq!(lens, vec![2, 0, 1]);
847 }
848}