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(&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    /// Set the generation prompt the grammar should be advanced past.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
260    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    /// Bias a token's logit. Use `f32::NEG_INFINITY` to ban it outright.
272    ///
273    /// # Errors
274    ///
275    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
276    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    /// Replace the sampler ordering.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
288    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    /// Replace the DRY sequence breakers (default: newline, `:`, `"`, `*`).
301    ///
302    /// # Errors
303    ///
304    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
305    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    /// Cap how many tokens the model may spend inside a reasoning block.
322    ///
323    /// `start` is the tokenized opening tag (`<think>`), `ends` the closing
324    /// tags — the first doubles as the forcing sequence. `forced` is emitted
325    /// when the budget runs out, typically a truncation message followed by the
326    /// closing tag. Set the budget itself through
327    /// [`CommonSamplerScalars::reasoning_budget_tokens`].
328    ///
329    /// # Errors
330    ///
331    /// Returns [`CommonSamplerError::Nul`] for an interior NUL in `message`.
332    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
361/// Flatten a slice of token sequences into `(data, lengths)`, which is how the
362/// shim takes ragged arrays without arrays-of-pointers.
363fn 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
373/// llama.cpp's assembled sampler chain.
374pub 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
386// SAFETY: the handle owns a `common_sampler` that is not shared; every method
387// takes `&mut self` where it mutates.
388unsafe 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    /// Assemble the chain for `model`.
398    ///
399    /// # Errors
400    ///
401    /// Returns [`CommonSamplerError::Failed`] if llama.cpp cannot build it —
402    /// most often a grammar that does not parse.
403    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    /// Sample a token from the logits at `idx`.
412    ///
413    /// `grammar_first` applies the grammar before the other samplers rather
414    /// than after. Upstream's default is `false`: sample first and only fall
415    /// back to grammar-constrained resampling if the pick is rejected, which is
416    /// much cheaper than filtering the whole vocabulary every step.
417    ///
418    /// # Errors
419    ///
420    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
421    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    /// Feed a token back into the sampler's history.
442    ///
443    /// `is_generated` marks a token the model produced, as opposed to one from
444    /// the prompt; penalties and the grammar treat the two differently.
445    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    /// Validate a speculative draft and return the accepted prefix.
450    ///
451    /// This is the acceptance half of speculative decoding: given `draft`
452    /// tokens proposed by a drafter and a target-model forward pass covering
453    /// them, it returns every draft token the target agrees with, plus one
454    /// freshly sampled token at the first divergence. The result is therefore
455    /// never empty and never longer than `draft.len() + 1`.
456    ///
457    /// Accepted tokens are also fed to [`Self::accept`] internally, so the
458    /// caller must not do so again.
459    ///
460    /// # Errors
461    ///
462    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
463    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    /// Clear all history and grammar state, keeping the configuration.
485    pub fn reset(&mut self) {
486        unsafe { sys::common_shim_sampler_reset(self.raw.as_ptr()) }
487    }
488
489    /// Deep-copy this sampler, state included.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
494    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    /// The seed actually in use, after any `LLAMA_DEFAULT_SEED` was resolved.
502    #[must_use]
503    pub fn seed(&self) -> u32 {
504        unsafe { sys::common_shim_sampler_get_seed(self.raw.as_ptr()) }
505    }
506
507    /// The most recently sampled token.
508    #[must_use]
509    pub fn last(&self) -> LlamaToken {
510        LlamaToken(unsafe { sys::common_shim_sampler_last(self.raw.as_ptr()) })
511    }
512
513    /// End the current reasoning block now, as if the budget had run out.
514    ///
515    /// Returns `false` when there is no budget sampler or it is not counting.
516    pub fn force_end_reasoning(&mut self) -> bool {
517        unsafe { sys::common_shim_sampler_reasoning_budget_force(self.raw.as_ptr()) }
518    }
519
520    /// Describe the assembled chain, e.g. `"penalties -> top_k -> temp"`.
521    ///
522    /// # Errors
523    ///
524    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
525    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    /// Detokenize the last `n` sampled tokens.
532    ///
533    /// # Errors
534    ///
535    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
536    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// ─────────────────────────────────────────────────────────────────────────────
551// Reasoning budget
552// ─────────────────────────────────────────────────────────────────────────────
553
554/// Where a [`ReasoningBudget`] is in its state machine.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum ReasoningBudgetState {
557    /// Passing tokens through, watching for a start tag.
558    Idle,
559    /// Inside a reasoning block, counting down.
560    Counting,
561    /// Budget spent; emitting the forced closing sequence.
562    Forcing,
563    /// Budget spent, but finishing a partial UTF-8 sequence first.
564    WaitingUtf8,
565    /// Finished; passing everything through.
566    Done,
567}
568
569impl ReasoningBudgetState {
570    // Unsigned in C, signed on the wire — bridge in both directions.
571    #[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/// A sampler that caps how long a model may think.
602///
603/// Reasoning models emit an unbounded `<think>…</think>` block before their
604/// answer, and nothing in the model stops them running to the context limit.
605/// This watches for the start tag, counts down, and — when the budget is spent
606/// — forces the closing sequence token by token, masking everything else.
607///
608/// It is the piece llama.cpp pairs with a *lazy* grammar: the grammar leaves
609/// reasoning unconstrained by design, so something else has to bound it.
610///
611/// Build one with [`ReasoningBudget::new`] and add it to a
612/// [`LlamaSampler::chain`](crate::sampling::LlamaSampler::chain), or let
613/// [`CommonSampler`] assemble it for you via
614/// [`CommonSamplerParams::set_reasoning_budget`].
615#[derive(Debug)]
616pub struct ReasoningBudget {
617    sampler: crate::sampling::LlamaSampler,
618}
619
620impl ReasoningBudget {
621    /// Build a budget sampler.
622    ///
623    /// * `starts` — tokenized opening tags; any one arms the countdown.
624    /// * `ends` — tokenized closing tags; any one disarms it naturally.
625    /// * `forced` — emitted when the budget runs out, usually a short message
626    ///   followed by a closing tag.
627    /// * `budget` — tokens allowed inside the block.
628    ///
629    /// # Errors
630    ///
631    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
632    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    /// [`Self::new`] with an explicit starting state — use
643    /// [`ReasoningBudgetState::Counting`] when the prompt already opened a
644    /// reasoning block, which is what a template with `thinking_forced_open`
645    /// produces.
646    ///
647    /// # Errors
648    ///
649    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
650    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        // SAFETY: upstream returns a sampler the caller owns and frees with
679        // `llama_sampler_free`, which is what `LlamaSampler` does.
680        Ok(Self {
681            sampler: unsafe { crate::sampling::LlamaSampler::from_raw_ptr(ptr) },
682        })
683    }
684
685    /// Where the state machine currently is.
686    #[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    /// Cut the reasoning block short now. Returns `false` if it was not
694    /// counting.
695    pub fn force_end(&mut self) -> bool {
696        unsafe { sys::common_shim_reasoning_budget_force(self.sampler.as_ptr()) }
697    }
698
699    /// Take the underlying sampler, to add to a chain.
700    #[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        // Pinned against upstream's documented defaults; a bump that changes
715        // them silently changes output for every caller.
716        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    /// Setting one field must not disturb the others — the getter/setter pair
740    /// copies the whole struct, so a missed field would silently reset.
741    #[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    /// The discriminants are an ABI contract with `common_sampler_type`; if
768    /// they drift, `set_samplers` silently reorders the chain.
769    #[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}