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}