Skip to main content

llama_cpp_4/
common_sampler.rs

1//! llama.cpp's assembled sampler chain, and the reasoning-budget sampler.
2//!
3//! [`LlamaSampler`](crate::sampling::LlamaSampler) wraps the individual
4//! `llama_sampler_*` primitives, leaving the caller to build a chain. This
5//! wraps `common_sampler` — the chain upstream assembles for its own tools —
6//! which is a different trade: less control, but it gets right several things
7//! that are easy to miss when hand-rolling.
8//!
9//! - **Ordering.** Penalties and DRY run before truncation samplers, which run
10//!   before temperature. A chain in the wrong order silently samples from the
11//!   wrong distribution.
12//! - **Grammar prefill.** Output-format and tool-call grammars are advanced
13//!   past the generation prompt; user grammars are not. See
14//!   [`ChatParams::grammar_sampler`](crate::chat::ChatParams::grammar_sampler)
15//!   for what goes wrong otherwise.
16//! - **Model-declared suppress tokens** are merged into the logit bias, so
17//!   `tokenizer.ggml.suppress_tokens` is honoured without the caller knowing it
18//!   exists.
19//! - **Reasoning budget**, created whenever a lazy grammar is active so a
20//!   thinking block can be force-closed.
21//! - **Speculative acceptance** via [`CommonSampler::sample_and_accept_n`].
22
23use 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
32/// Errors from the common-sampler layer.
33///
34/// An alias for [`ShimError`](crate::shim::ShimError) — every shim-backed
35/// module shares one error type, since they share one status enum and one error
36/// buffer.
37pub type CommonSamplerError = crate::shim::ShimError;
38
39use crate::shim::{check_status, last_error, read_i32s, read_string, read_tokens, Result};
40
41/// Which of llama.cpp's samplers to run, and in what order.
42///
43/// Values match `common_sampler_type`; the default chain is penalties → DRY →
44/// top-n-sigma → top-k → typical-p → top-p → min-p → XTC → temperature.
45// Discriminants are an ABI contract with `common_sampler_type`, and they are
46// not contiguous: 5 is a retired TFS-Z slot upstream left as a comment. Taken
47// verbatim from `common/common.h`; the round-trip test pins them.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[repr(i32)]
50pub enum CommonSamplerType {
51    /// DRY repetition penalty.
52    Dry = 1,
53    /// Top-k truncation.
54    TopK = 2,
55    /// Top-p (nucleus) truncation.
56    TopP = 3,
57    /// Min-p truncation.
58    MinP = 4,
59    /// Typical-p truncation.
60    TypicalP = 6,
61    /// Temperature scaling.
62    Temperature = 7,
63    /// Exclude Top Choices.
64    Xtc = 8,
65    /// Infill sampler, for fill-in-the-middle.
66    Infill = 9,
67    /// Repetition / frequency / presence penalties.
68    Penalties = 10,
69    /// Top-n-sigma truncation.
70    TopNSigma = 11,
71    /// Adaptive-p, which targets a probability rather than a cutoff.
72    AdaptiveP = 12,
73}
74
75impl CommonSamplerType {
76    /// llama.cpp's own name for this sampler, e.g. `"top_k"`.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`CommonSamplerError::Failed`] if llama.cpp cannot name it.
81    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    /// Parse llama.cpp's sampler names into an ordering.
88    ///
89    /// Unrecognised names are dropped by llama.cpp rather than rejected, so a
90    /// shorter result than `names` means something was not understood.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`CommonSamplerError::Nul`] if a name contains an interior NUL.
95    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/// How a grammar was obtained, which decides whether the generation prompt is
109/// prefilled into it.
110///
111/// Getting this wrong is silent: prefilling a user grammar consumes tokens it
112/// never expected, and *not* prefilling a tool-call grammar forces the model to
113/// re-emit the generation prompt.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum GrammarSource {
116    /// No grammar.
117    #[default]
118    None,
119    /// Supplied verbatim by the caller. Never prefilled.
120    User,
121    /// Generated from a JSON schema. Prefilled.
122    OutputFormat,
123    /// Generated by a chat template for tool calling. Prefilled.
124    ToolCalls,
125}
126
127impl GrammarSource {
128    // The C enum is unsigned; the field it feeds is `int32_t`.
129    #[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
141/// Parameters for [`CommonSampler::new`], seeded from llama.cpp's defaults.
142///
143/// The scalar knobs are a plain public struct ([`CommonSamplerScalars`]);
144/// everything backed by a container is set through a method, which keeps this
145/// wrapper stable when upstream adds fields.
146pub 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
158// SAFETY: the handle owns a `common_params_sampling` with no shared state; the
159// shim's only global is a thread-local error buffer.
160unsafe 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
168/// The scalar half of llama.cpp's sampling parameters.
169pub 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    /// Allocate with llama.cpp's defaults (temp 0.8, top-k 40, top-p 0.95, …).
179    ///
180    /// # Panics
181    ///
182    /// Panics if the allocation fails.
183    #[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    /// Read the scalar knobs.
192    #[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    /// Replace the scalar knobs. Read [`Self::scalars`] first and modify it, so
200    /// fields you do not care about keep llama.cpp's defaults.
201    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    /// Constrain sampling with a GBNF grammar.
206    ///
207    /// `source` decides whether the generation prompt is prefilled — see
208    /// [`GrammarSource`].
209    ///
210    /// # Errors
211    ///
212    /// Returns [`CommonSamplerError::Nul`] for an interior NUL in `grammar`.
213    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    /// Add a trigger that activates a lazy grammar.
227    ///
228    /// `kind` matches [`GrammarTrigger::kind`](crate::chat::GrammarTrigger):
229    /// `"token"`, `"word"`, `"pattern"` or `"pattern_full"`.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`CommonSamplerError::InvalidArg`] for an unknown `kind`, or
234    /// [`CommonSamplerError::Nul`] for an interior NUL.
235    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    /// Set the generation prompt the grammar should be advanced past.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
265    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    /// Bias a token's logit. Use `f32::NEG_INFINITY` to ban it outright.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
281    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    /// Replace the sampler ordering.
289    ///
290    /// # Errors
291    ///
292    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
293    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    /// Replace the DRY sequence breakers (default: newline, `:`, `"`, `*`).
302    ///
303    /// # Errors
304    ///
305    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
306    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    /// Cap how many tokens the model may spend inside a reasoning block.
323    ///
324    /// `start` is the tokenized opening tag (`<think>`), `ends` the closing
325    /// tags — the first doubles as the forcing sequence. `forced` is emitted
326    /// when the budget runs out, typically a truncation message followed by the
327    /// closing tag. Set the budget itself through
328    /// [`CommonSamplerScalars::reasoning_budget_tokens`].
329    ///
330    /// # Errors
331    ///
332    /// Returns [`CommonSamplerError::Nul`] for an interior NUL in `message`.
333    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
362/// Flatten a slice of token sequences into `(data, lengths)`, which is how the
363/// shim takes ragged arrays without arrays-of-pointers.
364fn 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
374/// llama.cpp's assembled sampler chain.
375pub 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
387// SAFETY: the handle owns a `common_sampler` that is not shared; every method
388// takes `&mut self` where it mutates.
389unsafe 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    /// Assemble the chain for `model`.
399    ///
400    /// # Errors
401    ///
402    /// Returns [`CommonSamplerError::Failed`] if llama.cpp cannot build it —
403    /// most often a grammar that does not parse.
404    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    /// Sample a token from the logits at `idx`.
413    ///
414    /// `grammar_first` applies the grammar before the other samplers rather
415    /// than after. Upstream's default is `false`: sample first and only fall
416    /// back to grammar-constrained resampling if the pick is rejected, which is
417    /// much cheaper than filtering the whole vocabulary every step.
418    ///
419    /// # Errors
420    ///
421    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
422    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    /// Feed a token back into the sampler's history.
443    ///
444    /// `is_generated` marks a token the model produced, as opposed to one from
445    /// the prompt; penalties and the grammar treat the two differently.
446    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    /// Validate a speculative draft and return the accepted prefix.
451    ///
452    /// This is the acceptance half of speculative decoding: given `draft`
453    /// tokens proposed by a drafter and a target-model forward pass covering
454    /// them, it returns every draft token the target agrees with, plus one
455    /// freshly sampled token at the first divergence. The result is therefore
456    /// never empty and never longer than `draft.len() + 1`.
457    ///
458    /// Accepted tokens are also fed to [`Self::accept`] internally, so the
459    /// caller must not do so again.
460    ///
461    /// # Errors
462    ///
463    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
464    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    /// Clear all history and grammar state, keeping the configuration.
486    pub fn reset(&mut self) {
487        unsafe { sys::common_shim_sampler_reset(self.raw.as_ptr()) }
488    }
489
490    /// Deep-copy this sampler, state included.
491    ///
492    /// # Errors
493    ///
494    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
495    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    /// The seed actually in use, after any `LLAMA_DEFAULT_SEED` was resolved.
503    #[must_use]
504    pub fn seed(&self) -> u32 {
505        unsafe { sys::common_shim_sampler_get_seed(self.raw.as_ptr()) }
506    }
507
508    /// The most recently sampled token.
509    #[must_use]
510    pub fn last(&self) -> LlamaToken {
511        LlamaToken(unsafe { sys::common_shim_sampler_last(self.raw.as_ptr()) })
512    }
513
514    /// End the current reasoning block now, as if the budget had run out.
515    ///
516    /// Returns `false` when there is no budget sampler or it is not counting.
517    pub fn force_end_reasoning(&mut self) -> bool {
518        unsafe { sys::common_shim_sampler_reasoning_budget_force(self.raw.as_ptr()) }
519    }
520
521    /// Describe the assembled chain, e.g. `"penalties -> top_k -> temp"`.
522    ///
523    /// # Errors
524    ///
525    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
526    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    /// Detokenize the last `n` sampled tokens.
533    ///
534    /// # Errors
535    ///
536    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
537    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// ─────────────────────────────────────────────────────────────────────────────
552// Reasoning budget
553// ─────────────────────────────────────────────────────────────────────────────
554
555/// Where a [`ReasoningBudget`] is in its state machine.
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557pub enum ReasoningBudgetState {
558    /// Passing tokens through, watching for a start tag.
559    Idle,
560    /// Inside a reasoning block, counting down.
561    Counting,
562    /// Budget spent; emitting the forced closing sequence.
563    Forcing,
564    /// Budget spent, but finishing a partial UTF-8 sequence first.
565    WaitingUtf8,
566    /// Finished; passing everything through.
567    Done,
568}
569
570impl ReasoningBudgetState {
571    // Unsigned in C, signed on the wire — bridge in both directions.
572    #[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/// A sampler that caps how long a model may think.
603///
604/// Reasoning models emit an unbounded `<think>…</think>` block before their
605/// answer, and nothing in the model stops them running to the context limit.
606/// This watches for the start tag, counts down, and — when the budget is spent
607/// — forces the closing sequence token by token, masking everything else.
608///
609/// It is the piece llama.cpp pairs with a *lazy* grammar: the grammar leaves
610/// reasoning unconstrained by design, so something else has to bound it.
611///
612/// Build one with [`ReasoningBudget::new`] and add it to a
613/// [`LlamaSampler::chain`](crate::sampling::LlamaSampler::chain), or let
614/// [`CommonSampler`] assemble it for you via
615/// [`CommonSamplerParams::set_reasoning_budget`].
616#[derive(Debug)]
617pub struct ReasoningBudget {
618    sampler: crate::sampling::LlamaSampler,
619}
620
621impl ReasoningBudget {
622    /// Build a budget sampler.
623    ///
624    /// * `starts` — tokenized opening tags; any one arms the countdown.
625    /// * `ends` — tokenized closing tags; any one disarms it naturally.
626    /// * `forced` — emitted when the budget runs out, usually a short message
627    ///   followed by a closing tag.
628    /// * `budget` — tokens allowed inside the block.
629    ///
630    /// # Errors
631    ///
632    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
633    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    /// [`Self::new`] with an explicit starting state — use
651    /// [`ReasoningBudgetState::Counting`] when the prompt already opened a
652    /// reasoning block, which is what a template with `thinking_forced_open`
653    /// produces.
654    ///
655    /// # Errors
656    ///
657    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
658    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        // SAFETY: upstream returns a sampler the caller owns and frees with
687        // `llama_sampler_free`, which is what `LlamaSampler` does.
688        Ok(Self {
689            sampler: unsafe { crate::sampling::LlamaSampler::from_raw_ptr(ptr) },
690        })
691    }
692
693    /// Where the state machine currently is.
694    #[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    /// Cut the reasoning block short now. Returns `false` if it was not
703    /// counting.
704    pub fn force_end(&mut self) -> bool {
705        unsafe { sys::common_shim_reasoning_budget_force(self.sampler.as_ptr()) }
706    }
707
708    /// Take the underlying sampler, to add to a chain.
709    #[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        // Pinned against upstream's documented defaults; a bump that changes
724        // them silently changes output for every caller.
725        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    /// Setting one field must not disturb the others — the getter/setter pair
749    /// copies the whole struct, so a missed field would silently reset.
750    #[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    /// The discriminants are an ABI contract with `common_sampler_type`; if
777    /// they drift, `set_samplers` silently reorders the chain.
778    #[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}