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        // SAFETY: the closure hands the shim a buffer pointer together with its capacity,
83        // and the shim reports how much it needed; nothing is read back unless it
84        // reported success.
85        read_string(|buf, len, expected| unsafe {
86            sys::common_shim_sampler_type_to_str(self as i32, buf, len, expected)
87        })
88    }
89
90    /// Parse llama.cpp's sampler names into an ordering.
91    ///
92    /// Unrecognised names are dropped by llama.cpp rather than rejected, so a
93    /// shorter result than `names` means something was not understood.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`CommonSamplerError::Nul`] if a name contains an interior NUL.
98    pub fn from_names(names: &[&str]) -> Result<Vec<i32>> {
99        let c_names: Vec<CString> = names
100            .iter()
101            .map(|n| CString::new(*n))
102            .collect::<std::result::Result<_, _>>()?;
103        let ptrs: Vec<*const c_char> = c_names.iter().map(|c| c.as_ptr()).collect();
104
105        // SAFETY: the closure hands the shim a buffer pointer together with its capacity,
106        // and the shim reports how much it needed; nothing is read back unless it
107        // reported success.
108        read_i32s(|out, cap, len| unsafe {
109            sys::common_shim_sampler_types_from_names(ptrs.as_ptr(), ptrs.len(), out, cap, len)
110        })
111    }
112}
113
114/// How a grammar was obtained, which decides whether the generation prompt is
115/// prefilled into it.
116///
117/// Getting this wrong is silent: prefilling a user grammar consumes tokens it
118/// never expected, and *not* prefilling a tool-call grammar forces the model to
119/// re-emit the generation prompt.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub enum GrammarSource {
122    /// No grammar.
123    #[default]
124    None,
125    /// Supplied verbatim by the caller. Never prefilled.
126    User,
127    /// Generated from a JSON schema. Prefilled.
128    OutputFormat,
129    /// Generated by a chat template for tool calling. Prefilled.
130    ToolCalls,
131}
132
133impl GrammarSource {
134    // The C enum is unsigned; the field it feeds is `int32_t`.
135    #[allow(clippy::cast_possible_wrap)]
136    fn as_raw(self) -> i32 {
137        let raw = match self {
138            Self::None => sys::COMMON_SHIM_GRAMMAR_NONE,
139            Self::User => sys::COMMON_SHIM_GRAMMAR_USER,
140            Self::OutputFormat => sys::COMMON_SHIM_GRAMMAR_OUTPUT_FORMAT,
141            Self::ToolCalls => sys::COMMON_SHIM_GRAMMAR_TOOL_CALLS,
142        };
143        raw as i32
144    }
145}
146
147/// Parameters for [`CommonSampler::new`], seeded from llama.cpp's defaults.
148///
149/// The scalar knobs are a plain public struct ([`CommonSamplerScalars`]);
150/// everything backed by a container is set through a method, which keeps this
151/// wrapper stable when upstream adds fields.
152pub struct CommonSamplerParams {
153    raw: NonNull<sys::common_shim_sampler_params>,
154}
155
156impl std::fmt::Debug for CommonSamplerParams {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("CommonSamplerParams")
159            .field("scalars", &self.scalars())
160            .finish_non_exhaustive()
161    }
162}
163
164// SAFETY: the handle owns a `common_params_sampling` with no shared state; the
165// shim's only global is a thread-local error buffer.
166unsafe impl Send for CommonSamplerParams {}
167
168impl Drop for CommonSamplerParams {
169    fn drop(&mut self) {
170        // SAFETY: this runs once from `Drop` on a non-null handle, and nothing reads it
171        // afterwards.
172        unsafe { sys::common_shim_sampler_params_free(self.raw.as_ptr()) }
173    }
174}
175
176/// The scalar half of llama.cpp's sampling parameters.
177pub type CommonSamplerScalars = sys::common_shim_sampler_scalars;
178
179impl Default for CommonSamplerParams {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl CommonSamplerParams {
186    /// Allocate with llama.cpp's defaults (temp 0.8, top-k 40, top-p 0.95, …).
187    ///
188    /// # Panics
189    ///
190    /// Panics if the allocation fails.
191    #[must_use]
192    pub fn new() -> Self {
193        // SAFETY: no borrowed pointer is involved beyond arguments alive for the call;
194        // the returned handle is checked for null before being wrapped.
195        let raw = unsafe { sys::common_shim_sampler_params_init() };
196        Self {
197            raw: NonNull::new(raw).expect("common_shim_sampler_params_init returned null"),
198        }
199    }
200
201    /// Read the scalar knobs.
202    #[must_use]
203    pub fn scalars(&self) -> CommonSamplerScalars {
204        // SAFETY: the struct is a plain-old-data `repr(C)` type with no pointer or
205        // reference fields, so an all-zero value is a valid instance; the shim
206        // fills it in immediately below.
207        let mut out: CommonSamplerScalars = unsafe { std::mem::zeroed() };
208        // SAFETY: the `CString` arguments are alive for the duration of the call, and any
209        // out-param is a stack local the shim writes through.
210        unsafe { sys::common_shim_sampler_params_get_scalars(self.raw.as_ptr(), &raw mut out) };
211        out
212    }
213
214    /// Replace the scalar knobs. Read [`Self::scalars`] first and modify it, so
215    /// fields you do not care about keep llama.cpp's defaults.
216    pub fn set_scalars(&mut self, scalars: &CommonSamplerScalars) {
217        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
218        // arguments are scalars the library validates itself.
219        unsafe { sys::common_shim_sampler_params_set_scalars(self.raw.as_ptr(), scalars) }
220    }
221
222    /// Constrain sampling with a GBNF grammar.
223    ///
224    /// `source` decides whether the generation prompt is prefilled — see
225    /// [`GrammarSource`].
226    ///
227    /// # Errors
228    ///
229    /// Returns [`CommonSamplerError::Nul`] for an interior NUL in `grammar`.
230    pub fn set_grammar(&mut self, grammar: &str, source: GrammarSource, lazy: bool) -> Result<()> {
231        let c_grammar = CString::new(grammar)?;
232        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
233        // arguments are scalars the library validates itself.
234        let status = unsafe {
235            sys::common_shim_sampler_params_set_grammar(
236                self.raw.as_ptr(),
237                c_grammar.as_ptr(),
238                source.as_raw(),
239                lazy,
240            )
241        };
242        check_status(status)
243    }
244
245    /// Add a trigger that activates a lazy grammar.
246    ///
247    /// `kind` matches [`GrammarTrigger::kind`](crate::chat::GrammarTrigger):
248    /// `"token"`, `"word"`, `"pattern"` or `"pattern_full"`.
249    ///
250    /// # Errors
251    ///
252    /// Returns [`CommonSamplerError::InvalidArg`] for an unknown `kind`, or
253    /// [`CommonSamplerError::Nul`] for an interior NUL.
254    pub fn add_grammar_trigger(
255        &mut self,
256        kind: &str,
257        value: &str,
258        token: LlamaToken,
259    ) -> Result<()> {
260        let raw_kind = match kind {
261            "token" => 0,
262            "word" => 1,
263            "pattern" => 2,
264            "pattern_full" => 3,
265            _ => return Err(CommonSamplerError::InvalidArg),
266        };
267        let c_value = CString::new(value)?;
268        // SAFETY: the `CString` arguments are alive for the duration of the call, and any
269        // out-param is a stack local the shim writes through.
270        let status = unsafe {
271            sys::common_shim_sampler_params_add_grammar_trigger(
272                self.raw.as_ptr(),
273                raw_kind,
274                c_value.as_ptr(),
275                token.0,
276            )
277        };
278        check_status(status)
279    }
280
281    /// Set the generation prompt the grammar should be advanced past.
282    ///
283    /// # Errors
284    ///
285    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
286    pub fn set_generation_prompt(&mut self, prompt: &str) -> Result<()> {
287        let c_prompt = CString::new(prompt)?;
288        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
289        // arguments are scalars the library validates itself.
290        let status = unsafe {
291            sys::common_shim_sampler_params_set_generation_prompt(
292                self.raw.as_ptr(),
293                c_prompt.as_ptr(),
294            )
295        };
296        check_status(status)
297    }
298
299    /// Bias a token's logit. Use `f32::NEG_INFINITY` to ban it outright.
300    ///
301    /// # Errors
302    ///
303    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
304    pub fn add_logit_bias(&mut self, token: LlamaToken, bias: f32) -> Result<()> {
305        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
306        // arguments are scalars the library validates itself.
307        let status = unsafe {
308            sys::common_shim_sampler_params_add_logit_bias(self.raw.as_ptr(), token.0, bias)
309        };
310        check_status(status)
311    }
312
313    /// Replace the sampler ordering.
314    ///
315    /// # Errors
316    ///
317    /// Returns [`CommonSamplerError::InvalidArg`] if the params handle is bad.
318    pub fn set_samplers(&mut self, samplers: &[CommonSamplerType]) -> Result<()> {
319        let raw: Vec<i32> = samplers.iter().map(|s| *s as i32).collect();
320        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
321        // arguments are scalars the library validates itself.
322        let status = unsafe {
323            sys::common_shim_sampler_params_set_samplers(self.raw.as_ptr(), raw.as_ptr(), raw.len())
324        };
325        check_status(status)
326    }
327
328    /// Replace the DRY sequence breakers (default: newline, `:`, `"`, `*`).
329    ///
330    /// # Errors
331    ///
332    /// Returns [`CommonSamplerError::Nul`] for an interior NUL.
333    pub fn set_dry_breakers(&mut self, breakers: &[&str]) -> Result<()> {
334        let c_breakers: Vec<CString> = breakers
335            .iter()
336            .map(|b| CString::new(*b))
337            .collect::<std::result::Result<_, _>>()?;
338        let ptrs: Vec<*const c_char> = c_breakers.iter().map(|c| c.as_ptr()).collect();
339        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
340        // arguments are scalars the library validates itself.
341        let status = unsafe {
342            sys::common_shim_sampler_params_set_dry_breakers(
343                self.raw.as_ptr(),
344                ptrs.as_ptr(),
345                ptrs.len(),
346            )
347        };
348        check_status(status)
349    }
350
351    /// Cap how many tokens the model may spend inside a reasoning block.
352    ///
353    /// `start` is the tokenized opening tag (`<think>`), `ends` the closing
354    /// tags — the first doubles as the forcing sequence. `forced` is emitted
355    /// when the budget runs out, typically a truncation message followed by the
356    /// closing tag. Set the budget itself through
357    /// [`CommonSamplerScalars::reasoning_budget_tokens`].
358    ///
359    /// # Errors
360    ///
361    /// Returns [`CommonSamplerError::Nul`] for an interior NUL in `message`.
362    pub fn set_reasoning_budget(
363        &mut self,
364        start: &[LlamaToken],
365        ends: &[Vec<LlamaToken>],
366        forced: &[LlamaToken],
367        message: &str,
368    ) -> Result<()> {
369        let start_raw: Vec<i32> = start.iter().map(|t| t.0).collect();
370        let (ends_flat, end_lens) = flatten(ends);
371        let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
372        let c_message = CString::new(message)?;
373
374        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
375        // arguments are scalars the library validates itself.
376        let status = unsafe {
377            sys::common_shim_sampler_params_set_reasoning_budget(
378                self.raw.as_ptr(),
379                start_raw.as_ptr(),
380                start_raw.len(),
381                ends_flat.as_ptr(),
382                end_lens.as_ptr(),
383                end_lens.len(),
384                forced_raw.as_ptr(),
385                forced_raw.len(),
386                c_message.as_ptr(),
387            )
388        };
389        check_status(status)
390    }
391}
392
393/// Flatten a slice of token sequences into `(data, lengths)`, which is how the
394/// shim takes ragged arrays without arrays-of-pointers.
395fn flatten(seqs: &[Vec<LlamaToken>]) -> (Vec<i32>, Vec<usize>) {
396    let mut data = Vec::new();
397    let mut lens = Vec::with_capacity(seqs.len());
398    for seq in seqs {
399        lens.push(seq.len());
400        data.extend(seq.iter().map(|t| t.0));
401    }
402    (data, lens)
403}
404
405/// llama.cpp's assembled sampler chain.
406pub struct CommonSampler {
407    raw: NonNull<sys::common_shim_sampler>,
408}
409
410impl std::fmt::Debug for CommonSampler {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        f.debug_struct("CommonSampler")
413            .field("seed", &self.seed())
414            .finish_non_exhaustive()
415    }
416}
417
418// SAFETY: the handle owns a `common_sampler` that is not shared; every method
419// takes `&mut self` where it mutates.
420unsafe impl Send for CommonSampler {}
421
422impl Drop for CommonSampler {
423    fn drop(&mut self) {
424        // SAFETY: this runs once from `Drop` on a non-null handle, and nothing reads it
425        // afterwards.
426        unsafe { sys::common_shim_sampler_free(self.raw.as_ptr()) }
427    }
428}
429
430impl CommonSampler {
431    /// Assemble the chain for `model`.
432    ///
433    /// # Errors
434    ///
435    /// Returns [`CommonSamplerError::Failed`] if llama.cpp cannot build it —
436    /// most often a grammar that does not parse.
437    pub fn new(model: &LlamaModel, params: &mut CommonSamplerParams) -> Result<Self> {
438        let raw =
439            // SAFETY: the arguments are either scalars or pointers that outlive the call, and
440            // the library owns whatever it returns.
441            unsafe { sys::common_shim_sampler_init(model.model.as_ptr(), params.raw.as_ptr()) };
442        NonNull::new(raw)
443            .map(|raw| Self { raw })
444            .ok_or_else(|| CommonSamplerError::Failed(last_error()))
445    }
446
447    /// Sample a token from the logits at `idx`.
448    ///
449    /// `grammar_first` applies the grammar before the other samplers rather
450    /// than after. Upstream's default is `false`: sample first and only fall
451    /// back to grammar-constrained resampling if the pick is rejected, which is
452    /// much cheaper than filtering the whole vocabulary every step.
453    ///
454    /// # Errors
455    ///
456    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
457    pub fn sample(
458        &mut self,
459        ctx: &mut LlamaContext<'_>,
460        idx: i32,
461        grammar_first: bool,
462    ) -> Result<LlamaToken> {
463        let mut status = sys::LLAMA_SHIM_OK;
464        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
465        // arguments are scalars the library validates itself.
466        let token = unsafe {
467            sys::common_shim_sampler_sample(
468                self.raw.as_ptr(),
469                ctx.context.as_ptr(),
470                idx,
471                grammar_first,
472                &raw mut status,
473            )
474        };
475        check_status(status)?;
476        Ok(LlamaToken(token))
477    }
478
479    /// Feed a token back into the sampler's history.
480    ///
481    /// `is_generated` marks a token the model produced, as opposed to one from
482    /// the prompt; penalties and the grammar treat the two differently.
483    pub fn accept(&mut self, token: LlamaToken, is_generated: bool) {
484        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
485        // arguments are scalars the library validates itself.
486        unsafe { sys::common_shim_sampler_accept(self.raw.as_ptr(), token.0, is_generated) }
487    }
488
489    /// Validate a speculative draft and return the accepted prefix.
490    ///
491    /// This is the acceptance half of speculative decoding: given `draft`
492    /// tokens proposed by a drafter and a target-model forward pass covering
493    /// them, it returns every draft token the target agrees with, plus one
494    /// freshly sampled token at the first divergence. The result is therefore
495    /// never empty and never longer than `draft.len() + 1`.
496    ///
497    /// Accepted tokens are also fed to [`Self::accept`] internally, so the
498    /// caller must not do so again.
499    ///
500    /// # Errors
501    ///
502    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
503    pub fn sample_and_accept_n(
504        &mut self,
505        ctx: &mut LlamaContext<'_>,
506        draft: &[LlamaToken],
507        grammar_first: bool,
508    ) -> Result<Vec<LlamaToken>> {
509        let raw_draft: Vec<i32> = draft.iter().map(|t| t.0).collect();
510        // SAFETY: the closure hands the shim a buffer pointer together with its capacity,
511        // and the shim reports how much it needed; nothing is read back unless it
512        // reported success.
513        read_tokens(|out, cap, len| unsafe {
514            sys::common_shim_sampler_sample_and_accept_n(
515                self.raw.as_ptr(),
516                ctx.context.as_ptr(),
517                raw_draft.as_ptr(),
518                raw_draft.len(),
519                grammar_first,
520                out,
521                cap,
522                len,
523            )
524        })
525    }
526
527    /// Clear all history and grammar state, keeping the configuration.
528    pub fn reset(&mut self) {
529        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
530        // arguments are scalars the library validates itself.
531        unsafe { sys::common_shim_sampler_reset(self.raw.as_ptr()) }
532    }
533
534    /// Deep-copy this sampler, state included.
535    ///
536    /// # Errors
537    ///
538    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
539    pub fn try_clone(&self) -> Result<Self> {
540        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
541        // arguments are scalars the library validates itself.
542        let raw = unsafe { sys::common_shim_sampler_clone(self.raw.as_ptr()) };
543        NonNull::new(raw)
544            .map(|raw| Self { raw })
545            .ok_or_else(|| CommonSamplerError::Failed(last_error()))
546    }
547
548    /// The seed actually in use, after any `LLAMA_DEFAULT_SEED` was resolved.
549    #[must_use]
550    pub fn seed(&self) -> u32 {
551        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
552        // arguments are scalars the library validates itself.
553        unsafe { sys::common_shim_sampler_get_seed(self.raw.as_ptr()) }
554    }
555
556    /// The most recently sampled token.
557    #[must_use]
558    pub fn last(&self) -> LlamaToken {
559        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
560        // arguments are scalars the library validates itself.
561        LlamaToken(unsafe { sys::common_shim_sampler_last(self.raw.as_ptr()) })
562    }
563
564    /// End the current reasoning block now, as if the budget had run out.
565    ///
566    /// Returns `false` when there is no budget sampler or it is not counting.
567    pub fn force_end_reasoning(&mut self) -> bool {
568        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
569        // arguments are scalars the library validates itself.
570        unsafe { sys::common_shim_sampler_reasoning_budget_force(self.raw.as_ptr()) }
571    }
572
573    /// Describe the assembled chain, e.g. `"penalties -> top_k -> temp"`.
574    ///
575    /// # Errors
576    ///
577    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
578    pub fn describe(&self) -> Result<String> {
579        // SAFETY: the closure hands the shim a buffer pointer together with its capacity,
580        // and the shim reports how much it needed; nothing is read back unless it
581        // reported success.
582        read_string(|buf, len, expected| unsafe {
583            sys::common_shim_sampler_print(self.raw.as_ptr(), buf, len, expected)
584        })
585    }
586
587    /// Detokenize the last `n` sampled tokens.
588    ///
589    /// # Errors
590    ///
591    /// Returns [`CommonSamplerError::Failed`] if llama.cpp throws.
592    pub fn prev_str(&mut self, ctx: &mut LlamaContext<'_>, n: i32) -> Result<String> {
593        // SAFETY: the closure hands the shim a buffer pointer together with its capacity,
594        // and the shim reports how much it needed; nothing is read back unless it
595        // reported success.
596        read_string(|buf, len, expected| unsafe {
597            sys::common_shim_sampler_prev_str(
598                self.raw.as_ptr(),
599                ctx.context.as_ptr(),
600                n,
601                buf,
602                len,
603                expected,
604            )
605        })
606    }
607}
608
609// ─────────────────────────────────────────────────────────────────────────────
610// Reasoning budget
611// ─────────────────────────────────────────────────────────────────────────────
612
613/// Where a [`ReasoningBudget`] is in its state machine.
614#[derive(Debug, Clone, Copy, PartialEq, Eq)]
615pub enum ReasoningBudgetState {
616    /// Passing tokens through, watching for a start tag.
617    Idle,
618    /// Inside a reasoning block, counting down.
619    Counting,
620    /// Budget spent; emitting the forced closing sequence.
621    Forcing,
622    /// Budget spent, but finishing a partial UTF-8 sequence first.
623    WaitingUtf8,
624    /// Finished; passing everything through.
625    Done,
626}
627
628impl ReasoningBudgetState {
629    // Unsigned in C, signed on the wire — bridge in both directions.
630    #[allow(clippy::cast_possible_wrap)]
631    fn from_raw(raw: i32) -> Option<Self> {
632        if raw == sys::COMMON_SHIM_RBUDGET_IDLE as i32 {
633            Some(Self::Idle)
634        } else if raw == sys::COMMON_SHIM_RBUDGET_COUNTING as i32 {
635            Some(Self::Counting)
636        } else if raw == sys::COMMON_SHIM_RBUDGET_FORCING as i32 {
637            Some(Self::Forcing)
638        } else if raw == sys::COMMON_SHIM_RBUDGET_WAITING_UTF8 as i32 {
639            Some(Self::WaitingUtf8)
640        } else if raw == sys::COMMON_SHIM_RBUDGET_DONE as i32 {
641            Some(Self::Done)
642        } else {
643            None
644        }
645    }
646
647    #[allow(clippy::cast_possible_wrap)]
648    fn as_raw(self) -> i32 {
649        let raw = match self {
650            Self::Idle => sys::COMMON_SHIM_RBUDGET_IDLE,
651            Self::Counting => sys::COMMON_SHIM_RBUDGET_COUNTING,
652            Self::Forcing => sys::COMMON_SHIM_RBUDGET_FORCING,
653            Self::WaitingUtf8 => sys::COMMON_SHIM_RBUDGET_WAITING_UTF8,
654            Self::Done => sys::COMMON_SHIM_RBUDGET_DONE,
655        };
656        raw as i32
657    }
658}
659
660/// A sampler that caps how long a model may think.
661///
662/// Reasoning models emit an unbounded `<think>…</think>` block before their
663/// answer, and nothing in the model stops them running to the context limit.
664/// This watches for the start tag, counts down, and — when the budget is spent
665/// — forces the closing sequence token by token, masking everything else.
666///
667/// It is the piece llama.cpp pairs with a *lazy* grammar: the grammar leaves
668/// reasoning unconstrained by design, so something else has to bound it.
669///
670/// Build one with [`ReasoningBudget::new`] and add it to a
671/// [`LlamaSampler::chain`](crate::sampling::LlamaSampler::chain), or let
672/// [`CommonSampler`] assemble it for you via
673/// [`CommonSamplerParams::set_reasoning_budget`].
674#[derive(Debug)]
675pub struct ReasoningBudget {
676    sampler: crate::sampling::LlamaSampler,
677}
678
679impl ReasoningBudget {
680    /// Build a budget sampler.
681    ///
682    /// * `starts` — tokenized opening tags; any one arms the countdown.
683    /// * `ends` — tokenized closing tags; any one disarms it naturally.
684    /// * `forced` — emitted when the budget runs out, usually a short message
685    ///   followed by a closing tag.
686    /// * `budget` — tokens allowed inside the block.
687    ///
688    /// # Errors
689    ///
690    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
691    pub fn new(
692        model: &LlamaModel,
693        starts: &[Vec<LlamaToken>],
694        ends: &[Vec<LlamaToken>],
695        forced: &[LlamaToken],
696        budget: i32,
697    ) -> Result<Self> {
698        Self::with_initial_state(
699            model,
700            starts,
701            ends,
702            forced,
703            budget,
704            ReasoningBudgetState::Idle,
705        )
706    }
707
708    /// [`Self::new`] with an explicit starting state — use
709    /// [`ReasoningBudgetState::Counting`] when the prompt already opened a
710    /// reasoning block, which is what a template with `thinking_forced_open`
711    /// produces.
712    ///
713    /// # Errors
714    ///
715    /// Returns [`CommonSamplerError::Failed`] if llama.cpp returns null.
716    pub fn with_initial_state(
717        model: &LlamaModel,
718        starts: &[Vec<LlamaToken>],
719        ends: &[Vec<LlamaToken>],
720        forced: &[LlamaToken],
721        budget: i32,
722        initial_state: ReasoningBudgetState,
723    ) -> Result<Self> {
724        let (starts_flat, start_lens) = flatten(starts);
725        let (ends_flat, end_lens) = flatten(ends);
726        let forced_raw: Vec<i32> = forced.iter().map(|t| t.0).collect();
727
728        // SAFETY: the arguments are either scalars or pointers that outlive the call, and
729        // the library owns whatever it returns.
730        let raw = unsafe {
731            sys::common_shim_reasoning_budget_init(
732                model.get_vocab().vocab.as_ref(),
733                starts_flat.as_ptr(),
734                start_lens.as_ptr(),
735                start_lens.len(),
736                ends_flat.as_ptr(),
737                end_lens.as_ptr(),
738                end_lens.len(),
739                forced_raw.as_ptr(),
740                forced_raw.len(),
741                budget,
742                initial_state.as_raw(),
743            )
744        };
745        let ptr = NonNull::new(raw).ok_or_else(|| CommonSamplerError::Failed(last_error()))?;
746        // SAFETY: upstream returns a sampler the caller owns and frees with
747        // `llama_sampler_free`, which is what `LlamaSampler` does.
748        Ok(Self {
749            // SAFETY: `from_raw_ptr` is `unsafe` because it takes ownership; `ptr` is the
750            // non-null sampler the shim just created and nothing else holds it.
751            sampler: unsafe { crate::sampling::LlamaSampler::from_raw_ptr(ptr) },
752        })
753    }
754
755    /// Where the state machine currently is.
756    #[must_use]
757    pub fn state(&self) -> Option<ReasoningBudgetState> {
758        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
759        // arguments are scalars the library validates itself.
760        let raw = unsafe {
761            sys::common_shim_reasoning_budget_get_state(self.sampler.as_ptr().cast_const())
762        };
763        ReasoningBudgetState::from_raw(raw)
764    }
765
766    /// Cut the reasoning block short now. Returns `false` if it was not
767    /// counting.
768    pub fn force_end(&mut self) -> bool {
769        // SAFETY: the handle is non-null and kept alive by this borrow, and the remaining
770        // arguments are scalars the library validates itself.
771        unsafe { sys::common_shim_reasoning_budget_force(self.sampler.as_ptr()) }
772    }
773
774    /// Take the underlying sampler, to add to a chain.
775    #[must_use]
776    pub fn into_sampler(self) -> crate::sampling::LlamaSampler {
777        self.sampler
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784
785    #[test]
786    fn params_start_from_llama_cpp_defaults() {
787        let params = CommonSamplerParams::new();
788        let s = params.scalars();
789        // Pinned against upstream's documented defaults; a bump that changes
790        // them silently changes output for every caller.
791        assert!((s.temp - 0.80).abs() < 1e-6, "temp = {}", s.temp);
792        assert_eq!(s.top_k, 40);
793        assert!((s.top_p - 0.95).abs() < 1e-6, "top_p = {}", s.top_p);
794        assert!((s.min_p - 0.05).abs() < 1e-6, "min_p = {}", s.min_p);
795        assert_eq!(s.mirostat, 0);
796        assert_eq!(s.reasoning_budget_tokens, -1, "budget disabled by default");
797    }
798
799    #[test]
800    fn scalars_round_trip() {
801        let mut params = CommonSamplerParams::new();
802        let mut s = params.scalars();
803        s.temp = 0.25;
804        s.top_k = 7;
805        s.reasoning_budget_tokens = 128;
806        params.set_scalars(&s);
807
808        let back = params.scalars();
809        assert!((back.temp - 0.25).abs() < 1e-6);
810        assert_eq!(back.top_k, 7);
811        assert_eq!(back.reasoning_budget_tokens, 128);
812    }
813
814    /// Setting one field must not disturb the others — the getter/setter pair
815    /// copies the whole struct, so a missed field would silently reset.
816    #[test]
817    fn setting_scalars_preserves_untouched_fields() {
818        let mut params = CommonSamplerParams::new();
819        let before = params.scalars();
820        let mut s = before;
821        s.top_k = 3;
822        params.set_scalars(&s);
823        let after = params.scalars();
824
825        assert_eq!(after.top_k, 3);
826        assert!((after.top_p - before.top_p).abs() < 1e-6);
827        assert!((after.dry_base - before.dry_base).abs() < 1e-6);
828        assert_eq!(after.penalty_last_n, before.penalty_last_n);
829        assert_eq!(after.seed, before.seed);
830    }
831
832    #[test]
833    fn sampler_type_names_match_upstream() {
834        assert_eq!(CommonSamplerType::TopK.name().unwrap(), "top_k");
835        assert_eq!(CommonSamplerType::TopP.name().unwrap(), "top_p");
836        assert_eq!(
837            CommonSamplerType::Temperature.name().unwrap(),
838            "temperature"
839        );
840    }
841
842    /// The discriminants are an ABI contract with `common_sampler_type`; if
843    /// they drift, `set_samplers` silently reorders the chain.
844    #[test]
845    fn sampler_type_discriminants_round_trip_through_names() {
846        for ty in [
847            CommonSamplerType::Dry,
848            CommonSamplerType::TopK,
849            CommonSamplerType::TopP,
850            CommonSamplerType::MinP,
851            CommonSamplerType::TypicalP,
852            CommonSamplerType::Temperature,
853            CommonSamplerType::Xtc,
854            CommonSamplerType::Infill,
855            CommonSamplerType::Penalties,
856            CommonSamplerType::TopNSigma,
857            CommonSamplerType::AdaptiveP,
858        ] {
859            let name = ty.name().unwrap();
860            let parsed = CommonSamplerType::from_names(&[&name]).unwrap();
861            assert_eq!(parsed, vec![ty as i32], "{name} did not round-trip");
862        }
863    }
864
865    #[test]
866    fn unknown_sampler_names_are_dropped() {
867        let parsed = CommonSamplerType::from_names(&["top_k", "not_a_sampler"]).unwrap();
868        assert_eq!(parsed, vec![CommonSamplerType::TopK as i32]);
869    }
870
871    #[test]
872    fn grammar_trigger_rejects_unknown_kind() {
873        let mut params = CommonSamplerParams::new();
874        assert!(matches!(
875            params.add_grammar_trigger("nonsense", "x", LlamaToken(-1)),
876            Err(CommonSamplerError::InvalidArg)
877        ));
878    }
879
880    #[test]
881    fn grammar_trigger_accepts_every_known_kind() {
882        let mut params = CommonSamplerParams::new();
883        for kind in ["token", "word", "pattern", "pattern_full"] {
884            params
885                .add_grammar_trigger(kind, "<tool_call>", LlamaToken(1))
886                .unwrap_or_else(|e| panic!("{kind} rejected: {e}"));
887        }
888    }
889
890    #[test]
891    fn interior_nul_is_rejected_not_truncated() {
892        let mut params = CommonSamplerParams::new();
893        assert!(matches!(
894            params.set_grammar("root ::= \0 \"a\"", GrammarSource::User, false),
895            Err(CommonSamplerError::Nul(_))
896        ));
897        assert!(matches!(
898            params.set_generation_prompt("a\0b"),
899            Err(CommonSamplerError::Nul(_))
900        ));
901    }
902
903    #[test]
904    fn flatten_produces_matching_data_and_lengths() {
905        let seqs = vec![
906            vec![LlamaToken(1), LlamaToken(2)],
907            vec![],
908            vec![LlamaToken(3)],
909        ];
910        let (data, lens) = flatten(&seqs);
911        assert_eq!(data, vec![1, 2, 3]);
912        assert_eq!(lens, vec![2, 0, 1]);
913    }
914}