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(&mut self, kind: &str, value: &str, token: LlamaToken) -> Result<()> {
236 let raw_kind = match kind {
237 "token" => 0,
238 "word" => 1,
239 "pattern" => 2,
240 "pattern_full" => 3,
241 _ => return Err(CommonSamplerError::InvalidArg),
242 };
243 let c_value = CString::new(value)?;
244 let status = unsafe {
245 sys::common_shim_sampler_params_add_grammar_trigger(
246 self.raw.as_ptr(),
247 raw_kind,
248 c_value.as_ptr(),
249 token.0,
250 )
251 };
252 check_status(status)
253 }
254
255 pub fn set_generation_prompt(&mut self, prompt: &str) -> Result<()> {
261 let c_prompt = CString::new(prompt)?;
262 let status = unsafe {
263 sys::common_shim_sampler_params_set_generation_prompt(
264 self.raw.as_ptr(),
265 c_prompt.as_ptr(),
266 )
267 };
268 check_status(status)
269 }
270
271 pub fn add_logit_bias(&mut self, token: LlamaToken, bias: f32) -> Result<()> {
277 let status = unsafe {
278 sys::common_shim_sampler_params_add_logit_bias(self.raw.as_ptr(), token.0, bias)
279 };
280 check_status(status)
281 }
282
283 pub fn set_samplers(&mut self, samplers: &[CommonSamplerType]) -> Result<()> {
289 let raw: Vec<i32> = samplers.iter().map(|s| *s as i32).collect();
290 let status = unsafe {
291 sys::common_shim_sampler_params_set_samplers(
292 self.raw.as_ptr(),
293 raw.as_ptr(),
294 raw.len(),
295 )
296 };
297 check_status(status)
298 }
299
300 pub fn set_dry_breakers(&mut self, breakers: &[&str]) -> Result<()> {
306 let c_breakers: Vec<CString> = breakers
307 .iter()
308 .map(|b| CString::new(*b))
309 .collect::<std::result::Result<_, _>>()?;
310 let ptrs: Vec<*const c_char> = c_breakers.iter().map(|c| c.as_ptr()).collect();
311 let status = unsafe {
312 sys::common_shim_sampler_params_set_dry_breakers(
313 self.raw.as_ptr(),
314 ptrs.as_ptr(),
315 ptrs.len(),
316 )
317 };
318 check_status(status)
319 }
320
321 pub fn set_reasoning_budget(
333 &mut self,
334 start: &[LlamaToken],
335 ends: &[Vec<LlamaToken>],
336 forced: &[LlamaToken],
337 message: &str,
338 ) -> Result<()> {
339 let start_raw: Vec<i32> = start.iter().map(|t| t.0).collect();
340 let (ends_flat, end_lens) = flatten(ends);
341 let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
342 let c_message = CString::new(message)?;
343
344 let status = unsafe {
345 sys::common_shim_sampler_params_set_reasoning_budget(
346 self.raw.as_ptr(),
347 start_raw.as_ptr(),
348 start_raw.len(),
349 ends_flat.as_ptr(),
350 end_lens.as_ptr(),
351 end_lens.len(),
352 forced_raw.as_ptr(),
353 forced_raw.len(),
354 c_message.as_ptr(),
355 )
356 };
357 check_status(status)
358 }
359}
360
361fn flatten(seqs: &[Vec<LlamaToken>]) -> (Vec<i32>, Vec<usize>) {
364 let mut data = Vec::new();
365 let mut lens = Vec::with_capacity(seqs.len());
366 for seq in seqs {
367 lens.push(seq.len());
368 data.extend(seq.iter().map(|t| t.0));
369 }
370 (data, lens)
371}
372
373pub struct CommonSampler {
375 raw: NonNull<sys::common_shim_sampler>,
376}
377
378impl std::fmt::Debug for CommonSampler {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 f.debug_struct("CommonSampler")
381 .field("seed", &self.seed())
382 .finish_non_exhaustive()
383 }
384}
385
386unsafe impl Send for CommonSampler {}
389
390impl Drop for CommonSampler {
391 fn drop(&mut self) {
392 unsafe { sys::common_shim_sampler_free(self.raw.as_ptr()) }
393 }
394}
395
396impl CommonSampler {
397 pub fn new(model: &LlamaModel, params: &mut CommonSamplerParams) -> Result<Self> {
404 let raw =
405 unsafe { sys::common_shim_sampler_init(model.model.as_ptr(), params.raw.as_ptr()) };
406 NonNull::new(raw)
407 .map(|raw| Self { raw })
408 .ok_or_else(|| CommonSamplerError::Failed(last_error()))
409 }
410
411 pub fn sample(
422 &mut self,
423 ctx: &mut LlamaContext<'_>,
424 idx: i32,
425 grammar_first: bool,
426 ) -> Result<LlamaToken> {
427 let mut status = sys::LLAMA_SHIM_OK;
428 let token = unsafe {
429 sys::common_shim_sampler_sample(
430 self.raw.as_ptr(),
431 ctx.context.as_ptr(),
432 idx,
433 grammar_first,
434 &raw mut status,
435 )
436 };
437 check_status(status)?;
438 Ok(LlamaToken(token))
439 }
440
441 pub fn accept(&mut self, token: LlamaToken, is_generated: bool) {
446 unsafe { sys::common_shim_sampler_accept(self.raw.as_ptr(), token.0, is_generated) }
447 }
448
449 pub fn sample_and_accept_n(
464 &mut self,
465 ctx: &mut LlamaContext<'_>,
466 draft: &[LlamaToken],
467 grammar_first: bool,
468 ) -> Result<Vec<LlamaToken>> {
469 let raw_draft: Vec<i32> = draft.iter().map(|t| t.0).collect();
470 read_tokens(|out, cap, len| unsafe {
471 sys::common_shim_sampler_sample_and_accept_n(
472 self.raw.as_ptr(),
473 ctx.context.as_ptr(),
474 raw_draft.as_ptr(),
475 raw_draft.len(),
476 grammar_first,
477 out,
478 cap,
479 len,
480 )
481 })
482 }
483
484 pub fn reset(&mut self) {
486 unsafe { sys::common_shim_sampler_reset(self.raw.as_ptr()) }
487 }
488
489 pub fn try_clone(&self) -> Result<Self> {
495 let raw = unsafe { sys::common_shim_sampler_clone(self.raw.as_ptr()) };
496 NonNull::new(raw)
497 .map(|raw| Self { raw })
498 .ok_or_else(|| CommonSamplerError::Failed(last_error()))
499 }
500
501 #[must_use]
503 pub fn seed(&self) -> u32 {
504 unsafe { sys::common_shim_sampler_get_seed(self.raw.as_ptr()) }
505 }
506
507 #[must_use]
509 pub fn last(&self) -> LlamaToken {
510 LlamaToken(unsafe { sys::common_shim_sampler_last(self.raw.as_ptr()) })
511 }
512
513 pub fn force_end_reasoning(&mut self) -> bool {
517 unsafe { sys::common_shim_sampler_reasoning_budget_force(self.raw.as_ptr()) }
518 }
519
520 pub fn describe(&self) -> Result<String> {
526 read_string(|buf, len, expected| unsafe {
527 sys::common_shim_sampler_print(self.raw.as_ptr(), buf, len, expected)
528 })
529 }
530
531 pub fn prev_str(&mut self, ctx: &mut LlamaContext<'_>, n: i32) -> Result<String> {
537 read_string(|buf, len, expected| unsafe {
538 sys::common_shim_sampler_prev_str(
539 self.raw.as_ptr(),
540 ctx.context.as_ptr(),
541 n,
542 buf,
543 len,
544 expected,
545 )
546 })
547 }
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum ReasoningBudgetState {
557 Idle,
559 Counting,
561 Forcing,
563 WaitingUtf8,
565 Done,
567}
568
569impl ReasoningBudgetState {
570 #[allow(clippy::cast_possible_wrap)]
572 fn from_raw(raw: i32) -> Option<Self> {
573 if raw == sys::COMMON_SHIM_RBUDGET_IDLE as i32 {
574 Some(Self::Idle)
575 } else if raw == sys::COMMON_SHIM_RBUDGET_COUNTING as i32 {
576 Some(Self::Counting)
577 } else if raw == sys::COMMON_SHIM_RBUDGET_FORCING as i32 {
578 Some(Self::Forcing)
579 } else if raw == sys::COMMON_SHIM_RBUDGET_WAITING_UTF8 as i32 {
580 Some(Self::WaitingUtf8)
581 } else if raw == sys::COMMON_SHIM_RBUDGET_DONE as i32 {
582 Some(Self::Done)
583 } else {
584 None
585 }
586 }
587
588 #[allow(clippy::cast_possible_wrap)]
589 fn as_raw(self) -> i32 {
590 let raw = match self {
591 Self::Idle => sys::COMMON_SHIM_RBUDGET_IDLE,
592 Self::Counting => sys::COMMON_SHIM_RBUDGET_COUNTING,
593 Self::Forcing => sys::COMMON_SHIM_RBUDGET_FORCING,
594 Self::WaitingUtf8 => sys::COMMON_SHIM_RBUDGET_WAITING_UTF8,
595 Self::Done => sys::COMMON_SHIM_RBUDGET_DONE,
596 };
597 raw as i32
598 }
599}
600
601#[derive(Debug)]
616pub struct ReasoningBudget {
617 sampler: crate::sampling::LlamaSampler,
618}
619
620impl ReasoningBudget {
621 pub fn new(
633 model: &LlamaModel,
634 starts: &[Vec<LlamaToken>],
635 ends: &[Vec<LlamaToken>],
636 forced: &[LlamaToken],
637 budget: i32,
638 ) -> Result<Self> {
639 Self::with_initial_state(model, starts, ends, forced, budget, ReasoningBudgetState::Idle)
640 }
641
642 pub fn with_initial_state(
651 model: &LlamaModel,
652 starts: &[Vec<LlamaToken>],
653 ends: &[Vec<LlamaToken>],
654 forced: &[LlamaToken],
655 budget: i32,
656 initial_state: ReasoningBudgetState,
657 ) -> Result<Self> {
658 let (starts_flat, start_lens) = flatten(starts);
659 let (ends_flat, end_lens) = flatten(ends);
660 let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
661
662 let raw = unsafe {
663 sys::common_shim_reasoning_budget_init(
664 model.get_vocab().vocab.as_ref(),
665 starts_flat.as_ptr(),
666 start_lens.as_ptr(),
667 start_lens.len(),
668 ends_flat.as_ptr(),
669 end_lens.as_ptr(),
670 end_lens.len(),
671 forced_raw.as_ptr(),
672 forced_raw.len(),
673 budget,
674 initial_state.as_raw(),
675 )
676 };
677 let ptr = NonNull::new(raw).ok_or_else(|| CommonSamplerError::Failed(last_error()))?;
678 Ok(Self {
681 sampler: unsafe { crate::sampling::LlamaSampler::from_raw_ptr(ptr) },
682 })
683 }
684
685 #[must_use]
687 pub fn state(&self) -> Option<ReasoningBudgetState> {
688 let raw =
689 unsafe { sys::common_shim_reasoning_budget_get_state(self.sampler.as_ptr().cast_const()) };
690 ReasoningBudgetState::from_raw(raw)
691 }
692
693 pub fn force_end(&mut self) -> bool {
696 unsafe { sys::common_shim_reasoning_budget_force(self.sampler.as_ptr()) }
697 }
698
699 #[must_use]
701 pub fn into_sampler(self) -> crate::sampling::LlamaSampler {
702 self.sampler
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 #[test]
711 fn params_start_from_llama_cpp_defaults() {
712 let params = CommonSamplerParams::new();
713 let s = params.scalars();
714 assert!((s.temp - 0.80).abs() < 1e-6, "temp = {}", s.temp);
717 assert_eq!(s.top_k, 40);
718 assert!((s.top_p - 0.95).abs() < 1e-6, "top_p = {}", s.top_p);
719 assert!((s.min_p - 0.05).abs() < 1e-6, "min_p = {}", s.min_p);
720 assert_eq!(s.mirostat, 0);
721 assert_eq!(s.reasoning_budget_tokens, -1, "budget disabled by default");
722 }
723
724 #[test]
725 fn scalars_round_trip() {
726 let mut params = CommonSamplerParams::new();
727 let mut s = params.scalars();
728 s.temp = 0.25;
729 s.top_k = 7;
730 s.reasoning_budget_tokens = 128;
731 params.set_scalars(&s);
732
733 let back = params.scalars();
734 assert!((back.temp - 0.25).abs() < 1e-6);
735 assert_eq!(back.top_k, 7);
736 assert_eq!(back.reasoning_budget_tokens, 128);
737 }
738
739 #[test]
742 fn setting_scalars_preserves_untouched_fields() {
743 let mut params = CommonSamplerParams::new();
744 let before = params.scalars();
745 let mut s = before;
746 s.top_k = 3;
747 params.set_scalars(&s);
748 let after = params.scalars();
749
750 assert_eq!(after.top_k, 3);
751 assert!((after.top_p - before.top_p).abs() < 1e-6);
752 assert!((after.dry_base - before.dry_base).abs() < 1e-6);
753 assert_eq!(after.penalty_last_n, before.penalty_last_n);
754 assert_eq!(after.seed, before.seed);
755 }
756
757 #[test]
758 fn sampler_type_names_match_upstream() {
759 assert_eq!(CommonSamplerType::TopK.name().unwrap(), "top_k");
760 assert_eq!(CommonSamplerType::TopP.name().unwrap(), "top_p");
761 assert_eq!(
762 CommonSamplerType::Temperature.name().unwrap(),
763 "temperature"
764 );
765 }
766
767 #[test]
770 fn sampler_type_discriminants_round_trip_through_names() {
771 for ty in [
772 CommonSamplerType::Dry,
773 CommonSamplerType::TopK,
774 CommonSamplerType::TopP,
775 CommonSamplerType::MinP,
776 CommonSamplerType::TypicalP,
777 CommonSamplerType::Temperature,
778 CommonSamplerType::Xtc,
779 CommonSamplerType::Infill,
780 CommonSamplerType::Penalties,
781 CommonSamplerType::TopNSigma,
782 CommonSamplerType::AdaptiveP,
783 ] {
784 let name = ty.name().unwrap();
785 let parsed = CommonSamplerType::from_names(&[&name]).unwrap();
786 assert_eq!(parsed, vec![ty as i32], "{name} did not round-trip");
787 }
788 }
789
790 #[test]
791 fn unknown_sampler_names_are_dropped() {
792 let parsed = CommonSamplerType::from_names(&["top_k", "not_a_sampler"]).unwrap();
793 assert_eq!(parsed, vec![CommonSamplerType::TopK as i32]);
794 }
795
796 #[test]
797 fn grammar_trigger_rejects_unknown_kind() {
798 let mut params = CommonSamplerParams::new();
799 assert!(matches!(
800 params.add_grammar_trigger("nonsense", "x", LlamaToken(-1)),
801 Err(CommonSamplerError::InvalidArg)
802 ));
803 }
804
805 #[test]
806 fn grammar_trigger_accepts_every_known_kind() {
807 let mut params = CommonSamplerParams::new();
808 for kind in ["token", "word", "pattern", "pattern_full"] {
809 params
810 .add_grammar_trigger(kind, "<tool_call>", LlamaToken(1))
811 .unwrap_or_else(|e| panic!("{kind} rejected: {e}"));
812 }
813 }
814
815 #[test]
816 fn interior_nul_is_rejected_not_truncated() {
817 let mut params = CommonSamplerParams::new();
818 assert!(matches!(
819 params.set_grammar("root ::= \0 \"a\"", GrammarSource::User, false),
820 Err(CommonSamplerError::Nul(_))
821 ));
822 assert!(matches!(
823 params.set_generation_prompt("a\0b"),
824 Err(CommonSamplerError::Nul(_))
825 ));
826 }
827
828 #[test]
829 fn flatten_produces_matching_data_and_lengths() {
830 let seqs = vec![
831 vec![LlamaToken(1), LlamaToken(2)],
832 vec![],
833 vec![LlamaToken(3)],
834 ];
835 let (data, lens) = flatten(&seqs);
836 assert_eq!(data, vec![1, 2, 3]);
837 assert_eq!(lens, vec![2, 0, 1]);
838 }
839}