Skip to main content

ik_llama_cpp_2/
grammar.rs

1//! Stateful GBNF grammar + DRY samplers.
2//!
3//! ik_llama.cpp exposes two *stateful* sampler objects that do not fit the
4//! stateless `llama_sample_*` array API modelled in [`crate::sampling`] (they
5//! own C-side state that advances as tokens are accepted):
6//!
7//! * [`LlamaGrammar`] — a GBNF grammar constraint. Unlike the stock
8//!   `llama-cpp-2` anchor (which parses GBNF in Rust and calls the now
9//!   commented-out `llama_grammar_init`), ik parses the grammar C-side via
10//!   `llama_sampler_init_grammar(vocab, grammar_str, root)`. The returned
11//!   grammar retains a pointer to the model's vocab, so it is lifetime-tied to
12//!   the [`LlamaModel`] it was built from.
13//! * [`LlamaDrySampler`] — the DRY ("Don't Repeat Yourself") repetition
14//!   sampler. It processes its sequence breakers against the vocab only at
15//!   init, so it does not retain the model.
16//!
17//! Both own a raw C object and free it on `Drop`. Typical use inside a
18//! generation loop: [`LlamaGrammar::apply`] / [`LlamaDrySampler::apply`] mutate
19//! the candidate array in place before you draw a token; then
20//! [`LlamaGrammar::accept_token`] / [`LlamaDrySampler::accept`] advance the
21//! sampler state with the token you drew.
22
23use std::ffi::CString;
24use std::marker::PhantomData;
25use std::os::raw::c_char;
26use std::ptr::NonNull;
27
28use ik_llama_cpp_sys as sys;
29
30use crate::context::LlamaContext;
31use crate::model::LlamaModel;
32use crate::sampling::LlamaTokenDataArray;
33use crate::token::LlamaToken;
34
35/// Error building a [`LlamaGrammar`].
36#[derive(Debug, thiserror::Error)]
37pub enum GrammarInitError {
38    /// The grammar string or its root symbol contained an interior NUL byte.
39    #[error("grammar string contained an interior NUL byte")]
40    Nul(#[from] std::ffi::NulError),
41    /// `llama_sampler_init_grammar` returned null: the GBNF failed to parse.
42    #[error("failed to parse GBNF grammar")]
43    Parse,
44}
45
46/// A stateful GBNF grammar constraint.
47///
48/// Build with [`LlamaGrammar::new`], then during generation call
49/// [`apply`](Self::apply) on the candidate array before sampling and
50/// [`accept_token`](Self::accept_token) with the token you drew to advance the
51/// grammar state.
52///
53/// Lifetime-tied to the [`LlamaModel`]: ik's grammar retains a pointer to the
54/// model's vocab (used to map tokens to text when accepting), so the model must
55/// outlive the grammar. This is enforced at compile time by the `'model`
56/// borrow.
57///
58/// The [`LlamaContext`] passed to [`apply`](Self::apply) /
59/// [`accept_token`](Self::accept_token) must belong to the **same model** this
60/// grammar was built from — `apply` reads the context's vocab while
61/// `accept_token` reads the grammar's stored vocab, so mixing models would
62/// constrain and advance against different vocabularies.
63#[derive(Debug)]
64pub struct LlamaGrammar<'model> {
65    grammar: NonNull<sys::llama_grammar>,
66    _model: PhantomData<&'model LlamaModel>,
67}
68
69impl<'model> LlamaGrammar<'model> {
70    /// Compile a GBNF grammar for `model`.
71    ///
72    /// `grammar_str` is GBNF source; `root` is the name of the start symbol
73    /// (conventionally `"root"`). Returns [`GrammarInitError::Parse`] if the
74    /// grammar fails to parse C-side.
75    ///
76    /// # Errors
77    ///
78    /// [`GrammarInitError::Nul`] if `grammar_str` or `root` contains an interior
79    /// NUL byte; [`GrammarInitError::Parse`] if the GBNF is invalid.
80    pub fn new(
81        model: &'model LlamaModel,
82        grammar_str: &str,
83        root: &str,
84    ) -> Result<Self, GrammarInitError> {
85        let c_grammar = CString::new(grammar_str)?;
86        let c_root = CString::new(root)?;
87        // SAFETY: `model` is a live, valid model; its vocab is const and lives
88        // as long as the model (which outlives `self` via the `'model` borrow).
89        let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
90        // SAFETY: `vocab` is valid; both C strings are valid for the call. ik
91        // parses `grammar_str`/`root` into rules (copying what it needs), so the
92        // `CString`s may drop after this returns. Null => parse failure.
93        let raw =
94            unsafe { sys::llama_sampler_init_grammar(vocab, c_grammar.as_ptr(), c_root.as_ptr()) };
95        NonNull::new(raw)
96            .map(|grammar| Self {
97                grammar,
98                _model: PhantomData,
99            })
100            .ok_or(GrammarInitError::Parse)
101    }
102
103    /// Apply the grammar's constraints to `arr` in place: tokens the grammar
104    /// cannot currently accept have their logit driven to `-inf`.
105    ///
106    /// Call this before sampling / drawing a token.
107    pub fn apply(&self, ctx: &mut LlamaContext, arr: &mut LlamaTokenDataArray) {
108        let mut c = arr.as_c();
109        // SAFETY: `self.grammar` and `ctx` are valid; `c` describes `arr.data`
110        // and is mutated in place through its pointer for the call.
111        unsafe {
112            sys::llama_grammar_apply(self.grammar.as_ptr(), ctx.as_ptr(), &mut c);
113        }
114    }
115
116    /// Advance the grammar state by accepting `token`.
117    ///
118    /// Call this after you draw a token, so subsequent [`apply`](Self::apply)
119    /// calls constrain the next position correctly.
120    pub fn accept_token(&mut self, ctx: &mut LlamaContext, token: LlamaToken) {
121        // SAFETY: `self.grammar` (exclusively borrowed) and `ctx` are valid.
122        unsafe {
123            sys::llama_grammar_accept_token(self.grammar.as_ptr(), ctx.as_ptr(), token.0);
124        }
125    }
126
127    /// Deep-copy this grammar (independent parse state), or `None` if the C-side
128    /// copy fails.
129    #[must_use]
130    pub fn try_clone(&self) -> Option<Self> {
131        // SAFETY: `self.grammar` is valid; `llama_grammar_copy` yields an
132        // independent grammar (or null on failure).
133        let raw = unsafe { sys::llama_grammar_copy(self.grammar.as_ptr()) };
134        NonNull::new(raw).map(|grammar| Self {
135            grammar,
136            _model: PhantomData,
137        })
138    }
139}
140
141impl Drop for LlamaGrammar<'_> {
142    fn drop(&mut self) {
143        // SAFETY: `self.grammar` was produced by `new`/`try_clone` and is freed
144        // exactly once (ownership is not shared).
145        unsafe { sys::llama_grammar_free(self.grammar.as_ptr()) };
146    }
147}
148
149/// Parameters for the DRY ("Don't Repeat Yourself") sampler.
150///
151/// DRY penalizes tokens that would extend a repeated sequence. See
152/// <https://github.com/oobabooga/text-generation-webui/pull/5677>.
153#[derive(Debug, Clone)]
154pub struct DryParams {
155    /// Penalty strength. `0.0` disables DRY.
156    pub multiplier: f32,
157    /// Exponential base for the length-scaled penalty (llama.cpp default `1.75`).
158    pub base: f32,
159    /// Minimum repeated-sequence length before a penalty applies (default `2`).
160    pub allowed_length: i32,
161    /// How many recent tokens to scan. `-1` = the whole context, `0` = disabled.
162    pub penalty_last_n: i32,
163    /// Strings that reset the repetition scan (e.g. newlines, quotes).
164    pub seq_breakers: Vec<String>,
165}
166
167impl Default for DryParams {
168    /// llama.cpp's stock defaults (DRY disabled via `multiplier = 0.0`).
169    fn default() -> Self {
170        Self {
171            multiplier: 0.0,
172            base: 1.75,
173            allowed_length: 2,
174            penalty_last_n: -1,
175            seq_breakers: ["\n", ":", "\"", "*"]
176                .iter()
177                .map(|s| (*s).to_string())
178                .collect(),
179        }
180    }
181}
182
183/// Error building a [`LlamaDrySampler`].
184#[derive(Debug, thiserror::Error)]
185pub enum DryInitError {
186    /// A sequence breaker contained an interior NUL byte.
187    #[error("a DRY sequence breaker contained an interior NUL byte")]
188    Nul(#[from] std::ffi::NulError),
189    /// `llama_sampler_init_dry` returned null.
190    #[error("failed to initialize the DRY sampler")]
191    Init,
192}
193
194/// The stateful DRY repetition sampler.
195///
196/// Build with [`LlamaDrySampler::new`], then [`apply`](Self::apply) it to the
197/// candidate array before sampling and [`accept`](Self::accept) each drawn
198/// token so its repetition history stays in sync.
199#[derive(Debug)]
200pub struct LlamaDrySampler {
201    dry: NonNull<sys::llama_sampler_dry>,
202}
203
204impl LlamaDrySampler {
205    /// Build a DRY sampler for `model` from `params`.
206    ///
207    /// # Errors
208    ///
209    /// [`DryInitError::Nul`] if a sequence breaker contains an interior NUL
210    /// byte; [`DryInitError::Init`] if the C-side initializer returns null.
211    pub fn new(model: &LlamaModel, params: &DryParams) -> Result<Self, DryInitError> {
212        // Own the breaker C strings and collect their pointers. ik copies each
213        // breaker into a `std::string` at init (and processes them against the
214        // vocab), so neither the `CString`s nor the model need outlive this call.
215        let c_breakers: Vec<CString> = params
216            .seq_breakers
217            .iter()
218            .map(|s| CString::new(s.as_str()))
219            .collect::<Result<_, _>>()?;
220        let mut ptrs: Vec<*const c_char> = c_breakers.iter().map(|c| c.as_ptr()).collect();
221        // SAFETY: `model` is a live, valid model; its vocab is const.
222        let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
223        // SAFETY: `vocab` is valid; `ptrs` is a `ptrs.len()`-long array of valid
224        // C strings that live for the call. ik only reads `seq_breakers`
225        // (declared `*mut` but not mutated). Null => init failure.
226        let raw = unsafe {
227            sys::llama_sampler_init_dry(
228                vocab,
229                params.multiplier,
230                params.base,
231                params.allowed_length,
232                params.penalty_last_n,
233                ptrs.as_mut_ptr(),
234                ptrs.len(),
235            )
236        };
237        NonNull::new(raw)
238            .map(|dry| Self { dry })
239            .ok_or(DryInitError::Init)
240    }
241
242    /// Apply the DRY penalty to `arr` in place, based on the accepted history.
243    ///
244    /// Call this before sampling / drawing a token.
245    pub fn apply(&mut self, ctx: &mut LlamaContext, arr: &mut LlamaTokenDataArray) {
246        let mut c = arr.as_c();
247        // SAFETY: `ctx` and `self.dry` are valid; `c` describes `arr.data`,
248        // mutated in place through its pointer.
249        unsafe { sys::llama_sample_dry(ctx.as_ptr(), self.dry.as_ptr(), &mut c) };
250    }
251
252    /// Record a drawn `token` in the sampler's repetition history.
253    pub fn accept(&mut self, token: LlamaToken) {
254        // SAFETY: `self.dry` is exclusively borrowed and valid.
255        unsafe { sys::llama_sampler_dry_accept(self.dry.as_ptr(), token.0) };
256    }
257
258    /// Clear the sampler's repetition history.
259    pub fn reset(&mut self) {
260        // SAFETY: `self.dry` is exclusively borrowed and valid.
261        unsafe { sys::llama_sampler_dry_reset(self.dry.as_ptr()) };
262    }
263
264    /// Deep-copy this sampler (independent history), or `None` if the C-side
265    /// copy fails.
266    #[must_use]
267    pub fn try_clone(&self) -> Option<Self> {
268        // SAFETY: `self.dry` is valid; `llama_sampler_dry_clone` yields an
269        // independent sampler (or null on failure).
270        let raw = unsafe { sys::llama_sampler_dry_clone(self.dry.as_ptr()) };
271        NonNull::new(raw).map(|dry| Self { dry })
272    }
273}
274
275impl Drop for LlamaDrySampler {
276    fn drop(&mut self) {
277        // SAFETY: `self.dry` was produced by `new`/`try_clone` and is freed
278        // exactly once (ownership is not shared).
279        unsafe { sys::llama_sampler_dry_free(self.dry.as_ptr()) };
280    }
281}
282
283/// Error converting a JSON Schema into a GBNF grammar via
284/// [`json_schema_to_grammar`] (the `common` feature).
285#[cfg(feature = "common")]
286#[derive(Debug, thiserror::Error)]
287pub enum JsonSchemaError {
288    /// The schema string contained an interior NUL byte.
289    #[error("JSON schema string contained an interior NUL byte")]
290    Nul(#[from] std::ffi::NulError),
291    /// The C-side conversion failed (invalid schema / parse error). The wrapped
292    /// value is the raw `llama_rs_status` code.
293    #[error("JSON schema to grammar conversion failed (status {0})")]
294    Convert(i32),
295    /// The produced grammar was not valid UTF-8 (should not happen).
296    #[error("converted grammar was not valid UTF-8")]
297    Utf8,
298}
299
300/// Convert a JSON Schema (given as a JSON string) into a GBNF grammar string,
301/// ready to pass to [`LlamaGrammar::new`].
302///
303/// Wraps ik's `common/json-schema-to-grammar` (hence the `common` feature), the
304/// same conversion `llama-server` uses. Useful for tool / function calling:
305/// convert a function's parameter schema into a grammar, build a
306/// [`LlamaGrammar`] from it, and constrain generation so the model can only emit
307/// arguments that satisfy the schema.
308///
309/// # Errors
310///
311/// [`JsonSchemaError::Nul`] if `schema_json` has an interior NUL byte;
312/// [`JsonSchemaError::Convert`] if the schema is invalid or fails to convert;
313/// [`JsonSchemaError::Utf8`] if the produced grammar is not valid UTF-8.
314#[cfg(feature = "common")]
315pub fn json_schema_to_grammar(schema_json: &str) -> Result<String, JsonSchemaError> {
316    let schema = CString::new(schema_json)?;
317    let mut out: *mut c_char = std::ptr::null_mut();
318    // SAFETY: `schema` is a valid C string; `out` is a valid out-pointer. On
319    // success the C side writes a heap-allocated NUL-terminated string to `out`
320    // (allocated by its matching allocator) that we free below.
321    let status =
322        unsafe { sys::ik_llama_rs_json_schema_to_grammar(schema.as_ptr(), false, &mut out) };
323    // LLAMA_RS_STATUS_OK == 0 (see wrapper_utils.h).
324    if status as i32 != 0 || out.is_null() {
325        // On a non-OK status the C side leaves `out` NULL; free defensively in
326        // case a future change sets it before failing.
327        if !out.is_null() {
328            // SAFETY: `out` was produced by the matching C allocator.
329            unsafe { sys::ik_llama_rs_string_free(out) };
330        }
331        return Err(JsonSchemaError::Convert(status as i32));
332    }
333    // SAFETY: `out` is a valid NUL-terminated C string that we own.
334    let bytes = unsafe { std::ffi::CStr::from_ptr(out) }.to_bytes().to_vec();
335    // SAFETY: `out` came from the matching C allocator and is freed exactly once.
336    unsafe { sys::ik_llama_rs_string_free(out) };
337    String::from_utf8(bytes).map_err(|_| JsonSchemaError::Utf8)
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn dry_params_default_is_disabled() {
346        let p = DryParams::default();
347        assert_eq!(p.multiplier, 0.0, "DRY is disabled by default");
348        assert_eq!(p.seq_breakers.len(), 4);
349        assert!(p.seq_breakers.iter().any(|b| b == "\n"));
350    }
351}