Skip to main content

llama_cpp_4/
model.rs

1//! A safe wrapper around `llama_model`.
2use std::ffi::CStr;
3use std::ffi::CString;
4use std::fmt;
5use std::num::NonZeroU16;
6use std::os::raw::{c_char, c_int};
7use std::path::Path;
8use std::ptr::NonNull;
9use std::slice;
10
11use llama_cpp_sys_4::{
12    llama_adapter_lora, llama_adapter_lora_init, llama_chat_apply_template,
13    llama_chat_builtin_templates, llama_chat_message, llama_detokenize, llama_init_from_model,
14    llama_model, llama_model_cls_label, llama_model_decoder_start_token, llama_model_desc,
15    llama_model_chat_template, llama_model_free, llama_model_ftype, llama_model_get_device,
16    llama_model_get_tok_embd, llama_model_get_vocab, llama_model_has_decoder,
17    llama_model_has_encoder, llama_model_is_diffusion, llama_model_is_hybrid,
18    llama_model_is_recurrent, llama_model_load_from_file, llama_model_load_from_splits,
19    llama_model_meta_count, llama_model_meta_key_by_index, llama_model_meta_val_str,
20    llama_model_meta_val_str_by_index, llama_model_n_cls_out, llama_model_n_ctx_train,
21    llama_model_n_devices, llama_model_n_embd, llama_model_n_embd_inp, llama_model_n_embd_out,
22    llama_model_n_expert, llama_model_n_head, llama_model_n_head_kv, llama_model_n_layer,
23    llama_model_n_layer_nextn, llama_model_n_params, llama_model_n_swa,
24    llama_model_rope_freq_scale_train, llama_model_rope_type, llama_model_save_to_file,
25    llama_model_size, llama_model_target_layer_ids, llama_model_target_layer_ids_n,
26    llama_split_path, llama_split_prefix, llama_token_to_piece, llama_tokenize, llama_vocab,
27    llama_vocab_type, LLAMA_VOCAB_TYPE_BPE, LLAMA_VOCAB_TYPE_SPM,
28};
29
30use crate::context::params::LlamaContextParams;
31use crate::context::LlamaContext;
32use crate::llama_backend::LlamaBackend;
33use crate::model::params::LlamaModelParams;
34use crate::quantize::LlamaFtype;
35use crate::token::LlamaToken;
36use crate::token_type::{LlamaTokenAttr, LlamaTokenAttrs};
37use crate::{
38    ApplyChatTemplateError, ChatTemplateError, LlamaContextLoadError, LlamaLoraAdapterInitError,
39    LlamaModelLoadError, NewLlamaChatMessageError, StringFromModelError, StringToTokenError,
40    TokenToStringError,
41};
42
43pub mod params;
44
45/// Opaque ggml backend device handle returned by [`LlamaModel::get_device`].
46///
47/// Use [`Self::name`], [`Self::description`], [`Self::device_type`], and
48/// [`Self::memory`] to inspect the device. The handle is valid for the lifetime
49/// of the parent [`LlamaModel`].
50#[derive(Debug, Copy, Clone, PartialEq, Eq)]
51pub struct LlamaBackendDevice {
52    pub(crate) dev: llama_cpp_sys_4::ggml_backend_dev_t,
53}
54
55/// Backend device class (CPU, discrete GPU, integrated GPU, …).
56//
57// `GGML_BACKEND_DEVICE_TYPE_*` are `c_uint` under clang/gcc and `c_int` under
58// MSVC. `as i32` compiles on both; the `cast_possible_wrap` allow covers the
59// clang/gcc case (the device-type values are small and never wrap).
60#[allow(clippy::cast_possible_wrap)]
61#[repr(i32)]
62#[derive(Copy, Clone, Debug, PartialEq, Eq)]
63pub enum LlamaBackendDeviceType {
64    /// Host CPU backend.
65    Cpu = llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_CPU as i32,
66    /// Discrete GPU.
67    Gpu = llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_GPU as i32,
68    /// Integrated GPU.
69    IntegratedGpu = llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_IGPU as i32,
70    /// Accelerator device (e.g. BLAS / Hexagon).
71    Accel = llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_ACCEL as i32,
72    /// Meta / placeholder device entry.
73    Meta = llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_META as i32,
74}
75
76impl From<llama_cpp_sys_4::ggml_backend_dev_type> for LlamaBackendDeviceType {
77    fn from(value: llama_cpp_sys_4::ggml_backend_dev_type) -> Self {
78        match value {
79            llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_CPU => Self::Cpu,
80            llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_GPU => Self::Gpu,
81            llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_IGPU => Self::IntegratedGpu,
82            llama_cpp_sys_4::GGML_BACKEND_DEVICE_TYPE_ACCEL => Self::Accel,
83            _ => Self::Meta,
84        }
85    }
86}
87
88impl LlamaBackendDevice {
89    /// Human-readable device name (e.g. `CUDA0`, `Metal`).
90    ///
91    /// # Errors
92    ///
93    /// Returns an error when the name pointer is null or not valid UTF-8.
94    pub fn name(&self) -> Result<&str, StringFromModelError> {
95        let ptr = unsafe { llama_cpp_sys_4::ggml_backend_dev_name(self.dev) };
96        if ptr.is_null() {
97            return Err(StringFromModelError::ReturnedError(-1));
98        }
99        let cstr = unsafe { CStr::from_ptr(ptr) };
100        cstr.to_str().map_err(StringFromModelError::Utf8Error)
101    }
102
103    /// Longer device description (often includes hardware name).
104    ///
105    /// # Errors
106    ///
107    /// Returns an error when the description pointer is null or not valid UTF-8.
108    pub fn description(&self) -> Result<&str, StringFromModelError> {
109        let ptr = unsafe { llama_cpp_sys_4::ggml_backend_dev_description(self.dev) };
110        if ptr.is_null() {
111            return Err(StringFromModelError::ReturnedError(-1));
112        }
113        let cstr = unsafe { CStr::from_ptr(ptr) };
114        cstr.to_str().map_err(StringFromModelError::Utf8Error)
115    }
116
117    /// Device class (CPU, GPU, integrated GPU, …).
118    #[must_use]
119    pub fn device_type(&self) -> LlamaBackendDeviceType {
120        unsafe { llama_cpp_sys_4::ggml_backend_dev_type(self.dev).into() }
121    }
122
123    /// Device memory `(free_bytes, total_bytes)`.
124    #[must_use]
125    pub fn memory(&self) -> (usize, usize) {
126        let mut free = 0usize;
127        let mut total = 0usize;
128        unsafe {
129            llama_cpp_sys_4::ggml_backend_dev_memory(self.dev, &raw mut free, &raw mut total);
130        }
131        (free, total)
132    }
133}
134
135/// Iterator over [`LlamaBackendDevice`] handles for a loaded model.
136///
137/// # Examples
138///
139/// ```no_run
140/// use llama_cpp_4::prelude::*;
141///
142/// fn main() {
143///     let backend = LlamaBackend::init().unwrap();
144///     let model = LlamaModel::load_from_file(&backend, "model.gguf", &LlamaModelParams::default()).unwrap();
145///     for dev in model.devices() {
146///         let (free, total) = dev.memory();
147///         println!("{}: {} / {} bytes free", dev.name().unwrap(), free, total);
148///     }
149/// }
150/// ```
151#[derive(Debug, Clone, Copy)]
152pub struct LlamaBackendDevices<'a> {
153    model: &'a LlamaModel,
154    next: i32,
155}
156
157#[allow(clippy::copy_iterator)]
158impl Iterator for LlamaBackendDevices<'_> {
159    type Item = LlamaBackendDevice;
160
161    fn next(&mut self) -> Option<Self::Item> {
162        let dev = self.model.get_device(self.next)?;
163        self.next += 1;
164        Some(dev)
165    }
166
167    fn size_hint(&self) -> (usize, Option<usize>) {
168        let remaining = usize::try_from((self.model.n_devices() - self.next).max(0)).unwrap_or(0);
169        (remaining, Some(remaining))
170    }
171}
172
173impl ExactSizeIterator for LlamaBackendDevices<'_> {}
174
175/// A safe wrapper around `llama_model`.
176#[derive(Debug)]
177#[repr(transparent)]
178#[allow(clippy::module_name_repetitions)]
179pub struct LlamaModel {
180    pub(crate) model: NonNull<llama_model>,
181}
182
183/// A safe wrapper around `llama_vocab`.
184#[derive(Debug)]
185#[repr(transparent)]
186#[allow(clippy::module_name_repetitions)]
187pub struct LlamaVocab {
188    pub(crate) vocab: NonNull<llama_vocab>,
189}
190
191impl LlamaVocab {
192    /// Get the number of tokens in the vocabulary.
193    #[must_use]
194    pub fn n_tokens(&self) -> i32 {
195        unsafe { llama_cpp_sys_4::llama_vocab_n_tokens(self.vocab.as_ref()) }
196    }
197
198    /// Get the vocabulary type.
199    ///
200    /// # Panics
201    ///
202    /// Panics if the C API returns a vocabulary type that does not fit in `u32`.
203    #[must_use]
204    pub fn vocab_type(&self) -> u32 {
205        unsafe { llama_cpp_sys_4::llama_vocab_type(self.vocab.as_ref()) as u32 }
206    }
207
208    /// Get the BOS token.
209    #[must_use]
210    pub fn bos(&self) -> LlamaToken {
211        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_bos(self.vocab.as_ref()) })
212    }
213
214    /// Get the EOS token.
215    #[must_use]
216    pub fn eos(&self) -> LlamaToken {
217        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_eos(self.vocab.as_ref()) })
218    }
219
220    /// Get the EOT (end of turn) token.
221    #[must_use]
222    pub fn eot(&self) -> LlamaToken {
223        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_eot(self.vocab.as_ref()) })
224    }
225
226    /// Get the CLS (classification) token.
227    #[must_use]
228    pub fn cls(&self) -> LlamaToken {
229        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_cls(self.vocab.as_ref()) })
230    }
231
232    /// Get the SEP (separator) token.
233    #[must_use]
234    pub fn sep(&self) -> LlamaToken {
235        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_sep(self.vocab.as_ref()) })
236    }
237
238    /// Get the NL (newline) token.
239    #[must_use]
240    pub fn nl(&self) -> LlamaToken {
241        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_nl(self.vocab.as_ref()) })
242    }
243
244    /// Get the PAD (padding) token.
245    #[must_use]
246    pub fn pad(&self) -> LlamaToken {
247        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_pad(self.vocab.as_ref()) })
248    }
249
250    /// Get the FIM prefix token.
251    #[must_use]
252    pub fn fim_pre(&self) -> LlamaToken {
253        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_pre(self.vocab.as_ref()) })
254    }
255
256    /// Get the FIM suffix token.
257    #[must_use]
258    pub fn fim_suf(&self) -> LlamaToken {
259        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_suf(self.vocab.as_ref()) })
260    }
261
262    /// Get the FIM middle token.
263    #[must_use]
264    pub fn fim_mid(&self) -> LlamaToken {
265        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_mid(self.vocab.as_ref()) })
266    }
267
268    /// Get the FIM padding token.
269    #[must_use]
270    pub fn fim_pad(&self) -> LlamaToken {
271        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_pad(self.vocab.as_ref()) })
272    }
273
274    /// Get the FIM repository token.
275    #[must_use]
276    pub fn fim_rep(&self) -> LlamaToken {
277        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_rep(self.vocab.as_ref()) })
278    }
279
280    /// Get the FIM separator token.
281    #[must_use]
282    pub fn fim_sep(&self) -> LlamaToken {
283        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_fim_sep(self.vocab.as_ref()) })
284    }
285
286    /// Check whether BOS should be added.
287    #[must_use]
288    pub fn get_add_bos(&self) -> bool {
289        unsafe { llama_cpp_sys_4::llama_vocab_get_add_bos(self.vocab.as_ref()) }
290    }
291
292    /// Check whether EOS should be added.
293    #[must_use]
294    pub fn get_add_eos(&self) -> bool {
295        unsafe { llama_cpp_sys_4::llama_vocab_get_add_eos(self.vocab.as_ref()) }
296    }
297
298    /// Check whether SEP should be added.
299    #[must_use]
300    pub fn get_add_sep(&self) -> bool {
301        unsafe { llama_cpp_sys_4::llama_vocab_get_add_sep(self.vocab.as_ref()) }
302    }
303
304    /// Tokens the model itself declares should never be sampled.
305    ///
306    /// Read from the GGUF key `tokenizer.ggml.suppress_tokens`. Whisper-style
307    /// models use this to bar non-speech tokens; most models declare none and
308    /// this returns an empty slice. Feed the result to
309    /// [`LlamaSampler::logit_bias`](crate::sampling::LlamaSampler::logit_bias)
310    /// with `f32::NEG_INFINITY` to enforce it.
311    ///
312    /// The slice borrows the vocab's own array; it is valid for as long as the
313    /// model is.
314    #[must_use]
315    pub fn suppress_tokens(&self) -> &[LlamaToken] {
316        let mut n: i32 = 0;
317        let ptr = unsafe {
318            llama_cpp_sys_4::llama_vocab_get_suppress_tokens(self.vocab.as_ref(), &raw mut n)
319        };
320        let Ok(n) = usize::try_from(n) else {
321            return &[];
322        };
323        if ptr.is_null() || n == 0 {
324            return &[];
325        }
326        // SAFETY: `LlamaToken` is `#[repr(transparent)]` over `llama_token`, so
327        // the arrays have identical layout. The pointer is owned by the vocab
328        // and outlives `&self`.
329        unsafe { std::slice::from_raw_parts(ptr.cast::<LlamaToken>(), n) }
330    }
331
332    /// Get the text representation of a token.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if the text pointer is null or not valid UTF-8.
337    pub fn get_text(&self, token: LlamaToken) -> Result<&str, StringFromModelError> {
338        let bytes = self.get_text_bytes(token)?;
339        std::str::from_utf8(bytes).map_err(StringFromModelError::Utf8Error)
340    }
341
342    /// Get the exact byte representation stored for a vocabulary token.
343    ///
344    /// Unlike [`Self::get_text`], this method does not require the token text
345    /// to be valid UTF-8.
346    ///
347    /// # Errors
348    ///
349    /// Returns an error if llama.cpp reports a null text pointer.
350    pub fn get_text_bytes(&self, token: LlamaToken) -> Result<&[u8], StringFromModelError> {
351        let ptr = unsafe { llama_cpp_sys_4::llama_vocab_get_text(self.vocab.as_ref(), token.0) };
352        if ptr.is_null() {
353            return Err(StringFromModelError::ReturnedError(-1));
354        }
355        let cstr = unsafe { CStr::from_ptr(ptr) };
356        Ok(cstr.to_bytes())
357    }
358
359    /// Get the score of a token.
360    #[must_use]
361    pub fn get_score(&self, token: LlamaToken) -> f32 {
362        unsafe { llama_cpp_sys_4::llama_vocab_get_score(self.vocab.as_ref(), token.0) }
363    }
364
365    /// Get the attributes of a token.
366    ///
367    /// # Panics
368    ///
369    /// Panics if the C API returns attributes that do not fit in `u32`.
370    #[must_use]
371    pub fn get_attr(&self, token: LlamaToken) -> u32 {
372        unsafe { llama_cpp_sys_4::llama_vocab_get_attr(self.vocab.as_ref(), token.0) as u32 }
373    }
374
375    /// Check if a token is a control token.
376    #[must_use]
377    pub fn is_control(&self, token: LlamaToken) -> bool {
378        unsafe { llama_cpp_sys_4::llama_vocab_is_control(self.vocab.as_ref(), token.0) }
379    }
380
381    /// Check if a token is an end-of-generation token.
382    #[must_use]
383    pub fn is_eog(&self, token: LlamaToken) -> bool {
384        unsafe { llama_cpp_sys_4::llama_vocab_is_eog(self.vocab.as_ref(), token.0) }
385    }
386
387    /// Get the token mask value for the vocabulary.
388    #[must_use]
389    pub fn mask(&self) -> LlamaToken {
390        LlamaToken(unsafe { llama_cpp_sys_4::llama_vocab_mask(self.vocab.as_ref()) })
391    }
392}
393
394/// A safe wrapper around `llama_adapter_lora`.
395#[derive(Debug)]
396#[repr(transparent)]
397#[allow(clippy::module_name_repetitions)]
398pub struct LlamaLoraAdapter {
399    pub(crate) lora_adapter: NonNull<llama_adapter_lora>,
400}
401
402impl LlamaLoraAdapter {
403    /// Get the number of metadata key-value pairs in the adapter.
404    #[must_use]
405    pub fn meta_count(&self) -> i32 {
406        unsafe { llama_cpp_sys_4::llama_adapter_meta_count(self.lora_adapter.as_ptr()) }
407    }
408
409    /// Get a metadata key by index.
410    ///
411    /// # Errors
412    ///
413    /// Returns an error if the index is out of range or the key is not valid UTF-8.
414    #[allow(clippy::cast_sign_loss)]
415    pub fn meta_key_by_index(
416        &self,
417        index: i32,
418        buf_size: usize,
419    ) -> Result<String, StringFromModelError> {
420        let mut buf = vec![0u8; buf_size];
421        let ret = unsafe {
422            llama_cpp_sys_4::llama_adapter_meta_key_by_index(
423                self.lora_adapter.as_ptr(),
424                index,
425                buf.as_mut_ptr().cast::<c_char>(),
426                buf_size,
427            )
428        };
429        if ret < 0 {
430            return Err(StringFromModelError::ReturnedError(ret));
431        }
432        let len = ret as usize;
433        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
434        Ok(s.to_owned())
435    }
436
437    /// Get a metadata value by key name.
438    ///
439    /// # Errors
440    ///
441    /// Returns an error if the key is not found or the value is not valid UTF-8.
442    #[allow(clippy::cast_sign_loss)]
443    pub fn meta_val_str(&self, key: &str, buf_size: usize) -> Result<String, StringFromModelError> {
444        let c_key = CString::new(key).map_err(|_| StringFromModelError::ReturnedError(-1))?;
445        let mut buf = vec![0u8; buf_size];
446        let ret = unsafe {
447            llama_cpp_sys_4::llama_adapter_meta_val_str(
448                self.lora_adapter.as_ptr(),
449                c_key.as_ptr(),
450                buf.as_mut_ptr().cast::<c_char>(),
451                buf_size,
452            )
453        };
454        if ret < 0 {
455            return Err(StringFromModelError::ReturnedError(ret));
456        }
457        let len = ret as usize;
458        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
459        Ok(s.to_owned())
460    }
461
462    /// Get a metadata value by index.
463    ///
464    /// # Errors
465    ///
466    /// Returns an error if the index is out of range or the value is not valid UTF-8.
467    #[allow(clippy::cast_sign_loss)]
468    pub fn meta_val_str_by_index(
469        &self,
470        index: i32,
471        buf_size: usize,
472    ) -> Result<String, StringFromModelError> {
473        let mut buf = vec![0u8; buf_size];
474        let ret = unsafe {
475            llama_cpp_sys_4::llama_adapter_meta_val_str_by_index(
476                self.lora_adapter.as_ptr(),
477                index,
478                buf.as_mut_ptr().cast::<c_char>(),
479                buf_size,
480            )
481        };
482        if ret < 0 {
483            return Err(StringFromModelError::ReturnedError(ret));
484        }
485        let len = ret as usize;
486        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
487        Ok(s.to_owned())
488    }
489
490    /// Get all metadata as a list of `(key, value)` pairs.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if any key or value cannot be read or is not valid UTF-8.
495    #[allow(clippy::cast_sign_loss)]
496    pub fn metadata(&self) -> Result<Vec<(String, String)>, StringFromModelError> {
497        let count = self.meta_count();
498        let mut result = Vec::with_capacity(count as usize);
499        for i in 0..count {
500            let key = self.meta_key_by_index(i, 256)?;
501            let val = self.meta_val_str_by_index(i, 4096)?;
502            result.push((key, val));
503        }
504        Ok(result)
505    }
506
507    /// Get the number of invocation tokens for this adapter.
508    #[must_use]
509    pub fn n_invocation_tokens(&self) -> u64 {
510        unsafe {
511            llama_cpp_sys_4::llama_adapter_get_alora_n_invocation_tokens(self.lora_adapter.as_ptr())
512        }
513    }
514
515    /// Get the invocation tokens for this adapter.
516    ///
517    /// Returns an empty slice if there are no invocation tokens.
518    #[must_use]
519    #[allow(clippy::cast_possible_truncation)]
520    pub fn invocation_tokens(&self) -> &[LlamaToken] {
521        let n = self.n_invocation_tokens() as usize;
522        if n == 0 {
523            return &[];
524        }
525        let ptr = unsafe {
526            llama_cpp_sys_4::llama_adapter_get_alora_invocation_tokens(self.lora_adapter.as_ptr())
527        };
528        if ptr.is_null() {
529            return &[];
530        }
531        // LlamaToken is repr(transparent) over llama_token (i32), so this cast is safe
532        unsafe { std::slice::from_raw_parts(ptr.cast::<LlamaToken>(), n) }
533    }
534}
535
536impl Drop for LlamaLoraAdapter {
537    fn drop(&mut self) {
538        unsafe {
539            llama_cpp_sys_4::llama_adapter_lora_free(self.lora_adapter.as_ptr());
540        }
541    }
542}
543
544/// A Safe wrapper around `llama_chat_message`
545#[derive(Debug, Eq, PartialEq, Clone)]
546pub struct LlamaChatMessage {
547    role: CString,
548    content: CString,
549}
550
551impl LlamaChatMessage {
552    /// Create a new `LlamaChatMessage`.
553    ///
554    /// # Errors
555    ///
556    /// Returns [`NewLlamaChatMessageError`] if the role or content contains a null byte.
557    pub fn new(role: String, content: String) -> Result<Self, NewLlamaChatMessageError> {
558        Ok(Self {
559            role: CString::new(role)?,
560            content: CString::new(content)?,
561        })
562    }
563}
564
565/// How to determine if we should prepend a bos token to tokens
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
567pub enum AddBos {
568    /// Add the beginning of stream token to the start of the string.
569    Always,
570    /// Do not add the beginning of stream token to the start of the string.
571    Never,
572}
573
574/// How to determine if we should tokenize special tokens
575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
576pub enum Special {
577    /// Allow tokenizing special and/or control tokens which otherwise are not exposed and treated as plaintext. Does not insert a leading space.
578    Tokenize,
579    /// Treat special and/or control tokens as plaintext.
580    Plaintext,
581}
582
583unsafe impl Send for LlamaModel {}
584
585unsafe impl Sync for LlamaModel {}
586
587impl LlamaModel {
588    /// Retrieves the vocabulary associated with the current Llama model.
589    ///
590    /// This method fetches the vocabulary from the underlying model using an unsafe
591    /// FFI call. The returned `LlamaVocab` struct contains a non-null pointer to
592    /// the vocabulary data, which is wrapped in a `NonNull` for safety.
593    ///
594    /// # Safety
595    /// This method uses an unsafe block to call a C function (`llama_model_get_vocab`),
596    /// which is assumed to return a valid pointer to the vocabulary. The caller should
597    /// ensure that the model object is properly initialized and valid before calling
598    /// this method, as dereferencing invalid pointers can lead to undefined behavior.
599    ///
600    /// # Returns
601    /// A `LlamaVocab` struct containing the vocabulary of the model.
602    ///
603    /// # Panics
604    ///
605    /// Panics if the underlying C function returns a null pointer.
606    ///
607    /// # Example
608    /// ```rust,ignore
609    /// let vocab = model.get_vocab();
610    /// ```
611    #[must_use]
612    pub fn get_vocab(&self) -> LlamaVocab {
613        let llama_vocab = unsafe { llama_model_get_vocab(self.model.as_ptr()) }.cast_mut();
614
615        LlamaVocab {
616            vocab: NonNull::new(llama_vocab).unwrap(),
617        }
618    }
619    /// Get the number of tokens the model was trained on.
620    ///
621    /// This function returns the number of tokens that the model was trained on, represented as a `u32`.
622    ///
623    /// # Panics
624    ///
625    /// This function will panic if the number of tokens the model was trained on does not fit into a `u32`.
626    /// This should be impossible on most platforms since llama.cpp returns a `c_int` (i32 on most platforms),
627    /// which is almost certainly positive.
628    #[must_use]
629    pub fn n_ctx_train(&self) -> u32 {
630        let n_ctx_train = unsafe { llama_model_n_ctx_train(self.model.as_ptr()) };
631        u32::try_from(n_ctx_train).expect("n_ctx_train fits into an u32")
632    }
633
634    /// Get all tokens in the model.
635    ///
636    /// This function returns an iterator over all the tokens in the model. Each item in the iterator is a tuple
637    /// containing a `LlamaToken` and its corresponding string representation (or an error if the conversion fails).
638    ///
639    /// # Parameters
640    ///
641    /// - `special`: The `Special` value that determines how special tokens (like BOS, EOS, etc.) are handled.
642    pub fn tokens(
643        &self,
644        special: Special,
645    ) -> impl Iterator<Item = (LlamaToken, Result<String, TokenToStringError>)> + '_ {
646        (0..self.n_vocab())
647            .map(LlamaToken::new)
648            .map(move |llama_token| (llama_token, self.token_to_str(llama_token, special)))
649    }
650
651    /// Get the beginning of stream token.
652    ///
653    /// This function returns the token that represents the beginning of a stream (BOS token).
654    #[must_use]
655    pub fn token_bos(&self) -> LlamaToken {
656        self.get_vocab().bos()
657    }
658
659    /// Get the end of stream token.
660    ///
661    /// This function returns the token that represents the end of a stream (EOS token).
662    #[must_use]
663    pub fn token_eos(&self) -> LlamaToken {
664        self.get_vocab().eos()
665    }
666
667    /// Get the newline token.
668    ///
669    /// This function returns the token that represents a newline character.
670    #[must_use]
671    pub fn token_nl(&self) -> LlamaToken {
672        self.get_vocab().nl()
673    }
674
675    /// Check if a token represents the end of generation (end of turn, end of sequence, etc.).
676    ///
677    /// This function returns `true` if the provided token signifies the end of generation or end of sequence,
678    /// such as EOS or other special tokens.
679    ///
680    /// # Parameters
681    ///
682    /// - `token`: The `LlamaToken` to check.
683    ///
684    /// # Returns
685    ///
686    /// - `true` if the token is an end-of-generation token, otherwise `false`.
687    #[must_use]
688    pub fn is_eog_token(&self, token: LlamaToken) -> bool {
689        self.get_vocab().is_eog(token)
690    }
691
692    /// Get the classification token.
693    #[must_use]
694    pub fn token_cls(&self) -> LlamaToken {
695        self.get_vocab().cls()
696    }
697
698    /// Get the end-of-turn token.
699    #[must_use]
700    pub fn token_eot(&self) -> LlamaToken {
701        self.get_vocab().eot()
702    }
703
704    /// Get the padding token.
705    #[must_use]
706    pub fn token_pad(&self) -> LlamaToken {
707        self.get_vocab().pad()
708    }
709
710    /// Get the separator token.
711    #[must_use]
712    pub fn token_sep(&self) -> LlamaToken {
713        self.get_vocab().sep()
714    }
715
716    /// Get the fill-in-the-middle prefix token.
717    #[must_use]
718    pub fn token_fim_pre(&self) -> LlamaToken {
719        self.get_vocab().fim_pre()
720    }
721
722    /// Get the fill-in-the-middle suffix token.
723    #[must_use]
724    pub fn token_fim_suf(&self) -> LlamaToken {
725        self.get_vocab().fim_suf()
726    }
727
728    /// Get the fill-in-the-middle middle token.
729    #[must_use]
730    pub fn token_fim_mid(&self) -> LlamaToken {
731        self.get_vocab().fim_mid()
732    }
733
734    /// Get the fill-in-the-middle padding token.
735    #[must_use]
736    pub fn token_fim_pad(&self) -> LlamaToken {
737        self.get_vocab().fim_pad()
738    }
739
740    /// Get the fill-in-the-middle repository token.
741    #[must_use]
742    pub fn token_fim_rep(&self) -> LlamaToken {
743        self.get_vocab().fim_rep()
744    }
745
746    /// Get the fill-in-the-middle separator token.
747    #[must_use]
748    pub fn token_fim_sep(&self) -> LlamaToken {
749        self.get_vocab().fim_sep()
750    }
751
752    /// Check if a token is a control token.
753    #[must_use]
754    pub fn token_is_control(&self, token: LlamaToken) -> bool {
755        self.get_vocab().is_control(token)
756    }
757
758    /// Get the score of a token.
759    #[must_use]
760    pub fn token_get_score(&self, token: LlamaToken) -> f32 {
761        self.get_vocab().get_score(token)
762    }
763
764    /// Get the raw text of a token.
765    ///
766    /// # Errors
767    ///
768    /// Returns an error if the token text is null or not valid UTF-8.
769    pub fn token_get_text(&self, token: LlamaToken) -> Result<&str, StringFromModelError> {
770        let ptr = unsafe {
771            llama_cpp_sys_4::llama_vocab_get_text(self.get_vocab().vocab.as_ref(), token.0)
772        };
773        if ptr.is_null() {
774            return Err(StringFromModelError::ReturnedError(-1));
775        }
776        let cstr = unsafe { CStr::from_ptr(ptr) };
777        cstr.to_str().map_err(StringFromModelError::Utf8Error)
778    }
779
780    /// Check if a BOS token should be added when tokenizing.
781    #[must_use]
782    pub fn add_bos_token(&self) -> bool {
783        self.get_vocab().get_add_bos()
784    }
785
786    /// Check if an EOS token should be added when tokenizing.
787    #[must_use]
788    pub fn add_eos_token(&self) -> bool {
789        self.get_vocab().get_add_eos()
790    }
791
792    /// Get the decoder start token.
793    ///
794    /// This function returns the token used to signal the start of decoding (i.e., the token used at the start
795    /// of a sequence generation).
796    #[must_use]
797    pub fn decode_start_token(&self) -> LlamaToken {
798        let token = unsafe { llama_model_decoder_start_token(self.model.as_ptr()) };
799        LlamaToken(token)
800    }
801
802    /// Convert a single token to a string.
803    ///
804    /// This function converts a `LlamaToken` into its string representation.
805    ///
806    /// # Errors
807    ///
808    /// This function returns an error if the token cannot be converted to a string. For more details, refer to
809    /// [`TokenToStringError`].
810    ///
811    /// # Parameters
812    ///
813    /// - `token`: The `LlamaToken` to convert.
814    /// - `special`: The `Special` value used to handle special tokens.
815    pub fn token_to_str(
816        &self,
817        token: LlamaToken,
818        special: Special,
819    ) -> Result<String, TokenToStringError> {
820        self.token_to_str_with_size(token, 32, special)
821    }
822
823    /// Convert a single token to bytes.
824    ///
825    /// This function converts a `LlamaToken` into a byte representation.
826    ///
827    /// # Errors
828    ///
829    /// This function returns an error if the token cannot be converted to bytes. For more details, refer to
830    /// [`TokenToStringError`].
831    ///
832    /// # Parameters
833    ///
834    /// - `token`: The `LlamaToken` to convert.
835    /// - `special`: The `Special` value used to handle special tokens.
836    pub fn token_to_bytes(
837        &self,
838        token: LlamaToken,
839        special: Special,
840    ) -> Result<Vec<u8>, TokenToStringError> {
841        self.token_to_bytes_with_size(token, 32, special, None)
842    }
843
844    /// Convert a single token to its raw llama.cpp piece bytes.
845    ///
846    /// Unlike [`LlamaModel::token_to_bytes`], this does not discard tokens based
847    /// on token attributes before calling llama.cpp. This is useful for runtimes
848    /// that must preserve control, byte, or other model-specific pieces exactly.
849    ///
850    /// This convenience form sizes the buffer automatically: it attempts a small
851    /// default buffer and, if llama.cpp reports it is too small, retries once
852    /// with the exact size llama.cpp requires. Use
853    /// [`LlamaModel::token_to_raw_bytes_with_size`] when you want explicit
854    /// control over the buffer (for example to reuse an allocation).
855    ///
856    /// # Errors
857    ///
858    /// This function returns an error if llama.cpp cannot convert the token.
859    pub fn token_to_raw_bytes(
860        &self,
861        token: LlamaToken,
862        special: Special,
863    ) -> Result<Vec<u8>, TokenToStringError> {
864        let mut buffer = Vec::with_capacity(32);
865        self.token_to_raw_bytes_into(token, special, &mut buffer)?;
866        Ok(buffer)
867    }
868
869    /// Converts one token to raw piece bytes in caller-owned reusable storage.
870    ///
871    /// # Errors
872    ///
873    /// Returns an error if llama.cpp reports an unknown token or an excessive
874    /// required buffer length.
875    pub fn token_to_raw_bytes_into(
876        &self,
877        token: LlamaToken,
878        special: Special,
879        buffer: &mut Vec<u8>,
880    ) -> Result<(), TokenToStringError> {
881        let special = matches!(special, Special::Tokenize);
882        buffer.clear();
883        if buffer.capacity() < 32 {
884            buffer.reserve(32);
885        }
886        loop {
887            let capacity = buffer.capacity();
888            buffer.resize(capacity, 0);
889            let native_capacity = c_int::try_from(capacity)
890                .map_err(|_| TokenToStringError::BufferCapacityExceeded(capacity))?;
891            let size = unsafe {
892                llama_token_to_piece(
893                    self.get_vocab().vocab.as_ref(),
894                    token.0,
895                    buffer.as_mut_ptr().cast::<c_char>(),
896                    native_capacity,
897                    0,
898                    special,
899                )
900            };
901            match size {
902                0 => {
903                    buffer.clear();
904                    return Err(TokenToStringError::UnknownTokenType);
905                }
906                needed if needed.is_negative() => {
907                    let required = usize::try_from(-needed)
908                        .map_err(|_| TokenToStringError::InsufficientBufferSpace(needed))?;
909                    buffer.clear();
910                    if required <= capacity {
911                        return Err(TokenToStringError::InsufficientBufferSpace(needed));
912                    }
913                    buffer.reserve_exact(required);
914                }
915                written => {
916                    let written = usize::try_from(written).map_err(|_| {
917                        TokenToStringError::NativePieceLength {
918                            returned: written,
919                            capacity,
920                        }
921                    })?;
922                    if written > capacity {
923                        buffer.clear();
924                        return Err(TokenToStringError::NativePieceLength {
925                            returned: size,
926                            capacity,
927                        });
928                    }
929                    buffer.truncate(written);
930                    return Ok(());
931                }
932            }
933        }
934    }
935
936    /// Convert a slice of tokens to their concatenated raw llama.cpp piece bytes.
937    ///
938    /// This is the batch counterpart to [`LlamaModel::token_to_raw_bytes`]: it
939    /// forwards each token directly to `llama_token_to_piece` without the
940    /// token-attribute filtering applied by [`LlamaModel::tokens_to_str`] /
941    /// [`LlamaModel::detokenize`], preserving control, byte, and other
942    /// model-specific pieces exactly. The bytes are not guaranteed to be valid
943    /// UTF-8 on their own, since a single codepoint may be split across several
944    /// byte-fallback tokens; see [`crate::token::detokenizer`] for incremental,
945    /// UTF-8-aware decoding.
946    ///
947    /// # Errors
948    ///
949    /// Returns an error if any token cannot be converted (see
950    /// [`LlamaModel::token_to_raw_bytes`]).
951    pub fn tokens_to_raw_bytes(
952        &self,
953        tokens: &[LlamaToken],
954        special: Special,
955    ) -> Result<Vec<u8>, TokenToStringError> {
956        let mut bytes = Vec::new();
957        for &token in tokens {
958            bytes.extend_from_slice(&self.token_to_raw_bytes(token, special)?);
959        }
960        Ok(bytes)
961    }
962
963    /// Convert a vector of tokens to a single string.
964    ///
965    /// This function takes a slice of `LlamaToken`s and converts them into a single string, concatenating their
966    /// string representations.
967    ///
968    /// # Errors
969    ///
970    /// This function returns an error if any token cannot be converted to a string. For more details, refer to
971    /// [`TokenToStringError`].
972    ///
973    /// # Parameters
974    ///
975    /// - `tokens`: A slice of `LlamaToken`s to convert.
976    /// - `special`: The `Special` value used to handle special tokens.
977    pub fn tokens_to_str(
978        &self,
979        tokens: &[LlamaToken],
980        special: Special,
981    ) -> Result<String, TokenToStringError> {
982        let mut builder = String::with_capacity(tokens.len() * 4);
983        for str in tokens
984            .iter()
985            .copied()
986            .map(|t| self.token_to_str(t, special))
987        {
988            builder += &str?;
989        }
990        Ok(builder)
991    }
992
993    /// Convert a string to a vector of tokens.
994    ///
995    /// This function converts a string into a vector of `LlamaToken`s. The function will tokenize the string
996    /// and return the corresponding tokens.
997    ///
998    /// # Errors
999    ///
1000    /// - This function will return an error if the input string contains a null byte.
1001    ///
1002    /// # Panics
1003    ///
1004    /// - This function will panic if the number of tokens exceeds `usize::MAX`.
1005    ///
1006    /// # Example
1007    ///
1008    /// ```no_run
1009    /// use llama_cpp_4::model::LlamaModel;
1010    ///
1011    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1012    /// use std::path::Path;
1013    /// use llama_cpp_4::model::AddBos;
1014    /// let backend = llama_cpp_4::llama_backend::LlamaBackend::init()?;
1015    /// let model = LlamaModel::load_from_file(&backend, Path::new("path/to/model"), &Default::default())?;
1016    /// let tokens = model.str_to_token("Hello, World!", AddBos::Always)?;
1017    /// # Ok(())
1018    /// # }
1019    /// ```
1020    pub fn str_to_token(
1021        &self,
1022        str: &str,
1023        add_bos: AddBos,
1024    ) -> Result<Vec<LlamaToken>, StringToTokenError> {
1025        let mut buffer = Vec::new();
1026        self.str_to_token_into(str, add_bos, &mut buffer)?;
1027        Ok(buffer)
1028    }
1029
1030    /// Tokenizes into caller-owned storage so repeated operations can reuse
1031    /// their allocation.
1032    ///
1033    /// # Errors
1034    ///
1035    /// Returns an error when the text contains NUL or its length exceeds the
1036    /// native integer representation.
1037    pub fn str_to_token_into(
1038        &self,
1039        str: &str,
1040        add_bos: AddBos,
1041        buffer: &mut Vec<LlamaToken>,
1042    ) -> Result<(), StringToTokenError> {
1043        self.str_to_token_into_with_special(str, add_bos, Special::Tokenize, buffer)
1044    }
1045
1046    /// Tokenize, choosing whether markup like `<|im_start|>` becomes the
1047    /// special token or literal text.
1048    ///
1049    /// [`Self::str_to_token`] always parses special tokens, which is right for
1050    /// text your own template produced and wrong for text a *user* supplied: if
1051    /// a user types `<|im_start|>assistant` and it tokenizes as the real
1052    /// control token, they have forged a turn. Pass [`Special::Plaintext`] for
1053    /// anything that came from outside.
1054    ///
1055    /// [`ChatParams::tokenize_prompt`](crate::chat::ChatParams::tokenize_prompt)
1056    /// does this for you across a rendered prompt, using llama.cpp's own
1057    /// per-span `is_input` flags.
1058    ///
1059    /// # Errors
1060    ///
1061    /// Returns an error when the text contains NUL or its length exceeds the
1062    /// native integer representation.
1063    pub fn str_to_token_with_special(
1064        &self,
1065        str: &str,
1066        add_bos: AddBos,
1067        special: Special,
1068    ) -> Result<Vec<LlamaToken>, StringToTokenError> {
1069        let mut buffer = Vec::new();
1070        self.str_to_token_into_with_special(str, add_bos, special, &mut buffer)?;
1071        Ok(buffer)
1072    }
1073
1074    /// [`Self::str_to_token_with_special`] into caller-owned storage.
1075    ///
1076    /// # Errors
1077    ///
1078    /// Returns an error when the text contains NUL or its length exceeds the
1079    /// native integer representation.
1080    pub fn str_to_token_into_with_special(
1081        &self,
1082        str: &str,
1083        add_bos: AddBos,
1084        special: Special,
1085        buffer: &mut Vec<LlamaToken>,
1086    ) -> Result<(), StringToTokenError> {
1087        let parse_special = matches!(special, Special::Tokenize);
1088        let add_bos = match add_bos {
1089            AddBos::Always => true,
1090            AddBos::Never => false,
1091        };
1092        if let Some(position) = str.as_bytes().iter().position(|byte| *byte == 0) {
1093            return Err(StringToTokenError::InteriorNul(position));
1094        }
1095        let tokens_estimation = std::cmp::max(8, (str.len() / 2) + usize::from(add_bos));
1096        buffer.clear();
1097        if buffer.capacity() < tokens_estimation {
1098            buffer.reserve(tokens_estimation);
1099        }
1100        let buffer_capacity = c_int::try_from(buffer.capacity())?;
1101        let text_length = c_int::try_from(str.len())?;
1102        let size = unsafe {
1103            llama_tokenize(
1104                self.get_vocab().vocab.as_ref(),
1105                str.as_ptr().cast(),
1106                text_length,
1107                buffer.as_mut_ptr().cast(),
1108                buffer_capacity,
1109                add_bos,
1110                parse_special,
1111            )
1112        };
1113
1114        // if we fail the first time we can resize the vector to the correct size and try again. This should never fail.
1115        // as a result - size is guaranteed to be positive here.
1116        let size = if size.is_negative() {
1117            let required = usize::try_from(size.unsigned_abs())?;
1118            if buffer.capacity() < required {
1119                buffer.reserve_exact(required);
1120            }
1121            let retry_capacity = c_int::try_from(buffer.capacity())?;
1122            unsafe {
1123                llama_tokenize(
1124                    self.get_vocab().vocab.as_ref(),
1125                    str.as_ptr().cast(),
1126                    text_length,
1127                    buffer.as_mut_ptr().cast(),
1128                    retry_capacity,
1129                    add_bos,
1130                    parse_special,
1131                )
1132            }
1133        } else {
1134            size
1135        };
1136
1137        let native_size = size;
1138        let size = usize::try_from(native_size)?;
1139        if size > buffer.capacity() {
1140            return Err(StringToTokenError::NativeTokenCount {
1141                returned: native_size,
1142                capacity: buffer.capacity(),
1143            });
1144        }
1145
1146        // Safety: `size <= capacity` and llama.cpp initialized elements up to `size`.
1147        unsafe { buffer.set_len(size) }
1148        Ok(())
1149    }
1150
1151    /// Counts tokens without materializing token identifiers.
1152    ///
1153    /// # Errors
1154    ///
1155    /// Returns an error when the text contains NUL or its length/count cannot
1156    /// be represented by the native API.
1157    pub fn str_token_count(&self, str: &str, add_bos: AddBos) -> Result<usize, StringToTokenError> {
1158        let add_bos = matches!(add_bos, AddBos::Always);
1159        if let Some(position) = str.as_bytes().iter().position(|byte| *byte == 0) {
1160            return Err(StringToTokenError::InteriorNul(position));
1161        }
1162        let size = unsafe {
1163            llama_tokenize(
1164                self.get_vocab().vocab.as_ref(),
1165                str.as_ptr().cast(),
1166                c_int::try_from(str.len())?,
1167                std::ptr::null_mut(),
1168                0,
1169                add_bos,
1170                true,
1171            )
1172        };
1173        Ok(usize::try_from(i64::from(size).unsigned_abs())?)
1174    }
1175
1176    /// Get the type of a token.
1177    ///
1178    /// This function retrieves the attributes associated with a given token. The attributes are typically used to
1179    /// understand whether the token represents a special type of token (e.g., beginning-of-sequence (BOS), end-of-sequence (EOS),
1180    /// control tokens, etc.).
1181    ///
1182    /// # Panics
1183    ///
1184    /// - This function will panic if the token type is unknown or cannot be converted to a valid `LlamaTokenAttrs`.
1185    ///
1186    /// # Example
1187    ///
1188    /// ```no_run
1189    /// use llama_cpp_4::model::LlamaModel;
1190    /// use llama_cpp_4::model::params::LlamaModelParams;
1191    /// use llama_cpp_4::llama_backend::LlamaBackend;
1192    /// use llama_cpp_4::token::LlamaToken;
1193    ///
1194    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1195    /// let backend = LlamaBackend::init()?;
1196    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1197    /// let token = LlamaToken::new(42);
1198    /// let token_attrs = model.token_attr(token);
1199    /// # Ok(())
1200    /// # }
1201    /// ```
1202    #[must_use]
1203    pub fn token_attr(&self, LlamaToken(id): LlamaToken) -> LlamaTokenAttrs {
1204        let token_type =
1205            unsafe { llama_cpp_sys_4::llama_vocab_get_attr(self.get_vocab().vocab.as_ref(), id) };
1206        LlamaTokenAttrs::try_from(token_type).expect("token type is valid")
1207    }
1208
1209    /// Detokenize a slice of tokens into a string.
1210    ///
1211    /// This is the inverse of [`str_to_token`](Self::str_to_token).
1212    ///
1213    /// # Parameters
1214    ///
1215    /// - `tokens`: The tokens to detokenize.
1216    /// - `remove_special`: If `true`, special tokens are removed from the output.
1217    /// - `unparse_special`: If `true`, special tokens are rendered as their text representation.
1218    ///
1219    /// # Errors
1220    ///
1221    /// Returns an error if the detokenized text is not valid UTF-8.
1222    #[allow(
1223        clippy::cast_possible_truncation,
1224        clippy::cast_possible_wrap,
1225        clippy::cast_sign_loss
1226    )]
1227    pub fn detokenize(
1228        &self,
1229        tokens: &[LlamaToken],
1230        remove_special: bool,
1231        unparse_special: bool,
1232    ) -> Result<String, StringFromModelError> {
1233        // First call with empty buffer to get required size
1234        let n_tokens = tokens.len() as i32;
1235        let token_ptr = tokens.as_ptr().cast::<llama_cpp_sys_4::llama_token>();
1236        let needed = unsafe {
1237            llama_detokenize(
1238                self.get_vocab().vocab.as_ref(),
1239                token_ptr,
1240                n_tokens,
1241                std::ptr::null_mut(),
1242                0,
1243                remove_special,
1244                unparse_special,
1245            )
1246        };
1247        // llama_detokenize returns negative required size when buffer is too small
1248        let buf_size = if needed < 0 {
1249            (-needed) as usize
1250        } else {
1251            needed as usize
1252        };
1253        let mut buf = vec![0u8; buf_size];
1254        let ret = unsafe {
1255            llama_detokenize(
1256                self.get_vocab().vocab.as_ref(),
1257                token_ptr,
1258                n_tokens,
1259                buf.as_mut_ptr().cast::<c_char>(),
1260                buf_size as i32,
1261                remove_special,
1262                unparse_special,
1263            )
1264        };
1265        if ret < 0 {
1266            return Err(StringFromModelError::ReturnedError(ret));
1267        }
1268        let len = ret as usize;
1269        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1270        Ok(s.to_owned())
1271    }
1272
1273    /// Convert a token to a string with a specified buffer size.
1274    ///
1275    /// This function allows you to convert a token into a string, with the ability to specify a buffer size for the operation.
1276    /// It is generally recommended to use `LlamaModel::token_to_str` instead, as 8 bytes is typically sufficient for most tokens,
1277    /// and the extra buffer size doesn't usually matter.
1278    ///
1279    /// # Errors
1280    ///
1281    /// - If the token type is unknown, an error will be returned.
1282    /// - If the resultant token exceeds the provided `buffer_size`, an error will occur.
1283    /// - If the token string returned by `llama-cpp` is not valid UTF-8, it will return an error.
1284    ///
1285    /// # Panics
1286    ///
1287    /// - This function will panic if the `buffer_size` does not fit into a `c_int`.
1288    /// - It will also panic if the size returned from `llama-cpp` does not fit into a `usize`, which should typically never happen.
1289    ///
1290    /// # Example
1291    ///
1292    /// ```no_run
1293    /// use llama_cpp_4::model::{LlamaModel, Special};
1294    /// use llama_cpp_4::model::params::LlamaModelParams;
1295    /// use llama_cpp_4::llama_backend::LlamaBackend;
1296    /// use llama_cpp_4::token::LlamaToken;
1297    ///
1298    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1299    /// let backend = LlamaBackend::init()?;
1300    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1301    /// let token = LlamaToken::new(42);
1302    /// let token_string = model.token_to_str_with_size(token, 32, Special::Plaintext)?;
1303    /// # Ok(())
1304    /// # }
1305    /// ```
1306    pub fn token_to_str_with_size(
1307        &self,
1308        token: LlamaToken,
1309        buffer_size: usize,
1310        special: Special,
1311    ) -> Result<String, TokenToStringError> {
1312        let bytes = self.token_to_bytes_with_size(token, buffer_size, special, None)?;
1313        Ok(String::from_utf8(bytes)?)
1314    }
1315
1316    /// Convert a token to bytes with a specified buffer size.
1317    ///
1318    /// Generally you should use [`LlamaModel::token_to_bytes`] instead as 8 bytes is enough for most words and
1319    /// the extra bytes do not really matter.
1320    ///
1321    /// # Errors
1322    ///
1323    /// - if the token type is unknown
1324    /// - the resultant token is larger than `buffer_size`.
1325    ///
1326    /// # Panics
1327    ///
1328    /// - This function will panic if `buffer_size` cannot fit into a `c_int`.
1329    /// - It will also panic if the size returned from `llama-cpp` cannot be converted to `usize` (which should not happen).
1330    ///
1331    /// # Example
1332    ///
1333    /// ```no_run
1334    /// use llama_cpp_4::model::{LlamaModel, Special};
1335    /// use llama_cpp_4::model::params::LlamaModelParams;
1336    /// use llama_cpp_4::llama_backend::LlamaBackend;
1337    /// use llama_cpp_4::token::LlamaToken;
1338    ///
1339    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1340    /// let backend = LlamaBackend::init()?;
1341    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1342    /// let token = LlamaToken::new(42);
1343    /// let token_bytes = model.token_to_bytes_with_size(token, 32, Special::Plaintext, None)?;
1344    /// # Ok(())
1345    /// # }
1346    /// ```
1347    pub fn token_to_bytes_with_size(
1348        &self,
1349        token: LlamaToken,
1350        buffer_size: usize,
1351        special: Special,
1352        lstrip: Option<NonZeroU16>,
1353    ) -> Result<Vec<u8>, TokenToStringError> {
1354        if token == self.token_nl() {
1355            return Ok(String::from("\n").into_bytes());
1356        }
1357
1358        // unsure what to do with this in the face of the 'special' arg + attr changes
1359        let attrs = self.token_attr(token);
1360        if (attrs.contains(LlamaTokenAttr::Control)
1361            && (token == self.token_bos() || token == self.token_eos()))
1362            || attrs.is_empty()
1363            || attrs
1364                .intersects(LlamaTokenAttr::Unknown | LlamaTokenAttr::Byte | LlamaTokenAttr::Unused)
1365        {
1366            return Ok(Vec::new());
1367        }
1368
1369        let special = match special {
1370            Special::Tokenize => true,
1371            Special::Plaintext => false,
1372        };
1373
1374        let string = CString::new(vec![b'*'; buffer_size]).expect("no null");
1375        let len = string.as_bytes().len();
1376        let len = c_int::try_from(len).expect("length fits into c_int");
1377        let buf = string.into_raw();
1378        let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
1379        let size = unsafe {
1380            llama_token_to_piece(
1381                self.get_vocab().vocab.as_ref(),
1382                token.0,
1383                buf,
1384                len,
1385                lstrip,
1386                special,
1387            )
1388        };
1389
1390        match size {
1391            0 => Err(TokenToStringError::UnknownTokenType),
1392            i if i.is_negative() => Err(TokenToStringError::InsufficientBufferSpace(i)),
1393            size => {
1394                let string = unsafe { CString::from_raw(buf) };
1395                let mut bytes = string.into_bytes();
1396                let len = usize::try_from(size).expect("size is positive and fits into usize");
1397                bytes.truncate(len);
1398                Ok(bytes)
1399            }
1400        }
1401    }
1402
1403    /// Convert a token to raw llama.cpp piece bytes with a specified buffer size.
1404    ///
1405    /// This intentionally bypasses the token-attribute filtering in
1406    /// [`LlamaModel::token_to_bytes_with_size`] and forwards directly to
1407    /// `llama_token_to_piece`.
1408    ///
1409    /// # Errors
1410    ///
1411    /// - if llama.cpp reports an unknown token type.
1412    /// - if the resultant token is larger than `buffer_size`.
1413    ///
1414    /// # Panics
1415    ///
1416    /// This function will panic if `buffer_size` cannot fit into a `c_int`.
1417    pub fn token_to_raw_bytes_with_size(
1418        &self,
1419        token: LlamaToken,
1420        buffer_size: usize,
1421        special: Special,
1422        lstrip: Option<NonZeroU16>,
1423    ) -> Result<Vec<u8>, TokenToStringError> {
1424        let special = match special {
1425            Special::Tokenize => true,
1426            Special::Plaintext => false,
1427        };
1428        let mut buffer = vec![0_u8; buffer_size];
1429        let len = c_int::try_from(buffer.len()).expect("length fits into c_int");
1430        let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
1431        let size = unsafe {
1432            llama_token_to_piece(
1433                self.get_vocab().vocab.as_ref(),
1434                token.0,
1435                buffer.as_mut_ptr().cast::<c_char>(),
1436                len,
1437                lstrip,
1438                special,
1439            )
1440        };
1441
1442        match size {
1443            0 => Err(TokenToStringError::UnknownTokenType),
1444            i if i.is_negative() => Err(TokenToStringError::InsufficientBufferSpace(i)),
1445            size => {
1446                let len = usize::try_from(size).expect("size is positive and fits into usize");
1447                buffer.truncate(len);
1448                Ok(buffer)
1449            }
1450        }
1451    }
1452    /// The number of tokens the model was trained on.
1453    ///
1454    /// This function returns the number of tokens the model was trained on. It is returned as a `c_int` for maximum
1455    /// compatibility with the underlying llama-cpp library, though it can typically be cast to an `i32` without issue.
1456    ///
1457    /// # Example
1458    ///
1459    /// ```no_run
1460    /// use llama_cpp_4::model::LlamaModel;
1461    /// use llama_cpp_4::model::params::LlamaModelParams;
1462    /// use llama_cpp_4::llama_backend::LlamaBackend;
1463    ///
1464    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1465    /// let backend = LlamaBackend::init()?;
1466    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1467    /// let n_vocab = model.n_vocab();
1468    /// # Ok(())
1469    /// # }
1470    /// ```
1471    #[must_use]
1472    pub fn n_vocab(&self) -> i32 {
1473        self.get_vocab().n_tokens()
1474    }
1475
1476    /// The type of vocab the model was trained on.
1477    ///
1478    /// This function returns the type of vocabulary used by the model, such as whether it is based on byte-pair encoding (BPE),
1479    /// word-level tokens, or another tokenization scheme.
1480    ///
1481    /// # Panics
1482    ///
1483    /// - This function will panic if `llama-cpp` emits a vocab type that is not recognized or is invalid for this library.
1484    ///
1485    /// # Example
1486    ///
1487    /// ```no_run
1488    /// use llama_cpp_4::model::LlamaModel;
1489    /// use llama_cpp_4::model::params::LlamaModelParams;
1490    /// use llama_cpp_4::llama_backend::LlamaBackend;
1491    ///
1492    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1493    /// let backend = LlamaBackend::init()?;
1494    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1495    /// let vocab_type = model.vocab_type();
1496    /// # Ok(())
1497    /// # }
1498    /// ```
1499    #[must_use]
1500    pub fn vocab_type(&self) -> VocabType {
1501        let vocab_type = unsafe { llama_vocab_type(self.get_vocab().vocab.as_ref()) };
1502        VocabType::try_from(vocab_type).expect("invalid vocab type")
1503    }
1504
1505    /// Returns the number of embedding dimensions for the model.
1506    ///
1507    /// This function retrieves the number of embeddings (or embedding dimensions) used by the model. It is typically
1508    /// used for analyzing model architecture and setting up context parameters or other model configuration aspects.
1509    ///
1510    /// # Panics
1511    ///
1512    /// - This function may panic if the underlying `llama-cpp` library returns an invalid embedding dimension value.
1513    ///
1514    /// # Example
1515    ///
1516    /// ```no_run
1517    /// use llama_cpp_4::model::LlamaModel;
1518    /// use llama_cpp_4::model::params::LlamaModelParams;
1519    /// use llama_cpp_4::llama_backend::LlamaBackend;
1520    ///
1521    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1522    /// let backend = LlamaBackend::init()?;
1523    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1524    /// let n_embd = model.n_embd();
1525    /// # Ok(())
1526    /// # }
1527    /// ```
1528    #[must_use]
1529    pub fn n_embd(&self) -> c_int {
1530        unsafe { llama_model_n_embd(self.model.as_ptr()) }
1531    }
1532
1533    /// Get the number of transformer layers in the model.
1534    #[must_use]
1535    pub fn n_layer(&self) -> c_int {
1536        unsafe { llama_model_n_layer(self.model.as_ptr()) }
1537    }
1538
1539    /// Get the number of `NextN` / MTP prediction heads bundled with the model.
1540    ///
1541    /// Returns `0` when the checkpoint has no `NextN` blocks. Multi-head models
1542    /// (e.g. Step3.5) return values greater than `1`; pair with
1543    /// [`crate::context::LlamaContext::set_nextn_layer_offset`] on the draft
1544    /// context. See [`crate::mtp`] for the speculative-decoding workflow.
1545    ///
1546    /// # Examples
1547    ///
1548    /// ```no_run
1549    /// # use llama_cpp_4::llama_backend::LlamaBackend;
1550    /// # use llama_cpp_4::model::{LlamaModel, params::LlamaModelParams};
1551    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1552    /// # let backend = LlamaBackend::init()?;
1553    /// # let model = LlamaModel::load_from_file(&backend, "model.gguf", &LlamaModelParams::default())?;
1554    /// if model.n_layer_nextn() > 0 {
1555    ///     println!("MTP model with {} NextN heads", model.n_layer_nextn());
1556    /// }
1557    /// # Ok(())
1558    /// # }
1559    /// ```
1560    #[must_use]
1561    pub fn n_layer_nextn(&self) -> c_int {
1562        unsafe { llama_model_n_layer_nextn(self.model.as_ptr()) }
1563    }
1564
1565    /// Get the number of mixture-of-experts (`MoE`) layers in the model.
1566    ///
1567    /// Returns `0` for dense (non-MoE) checkpoints.
1568    #[must_use]
1569    pub fn n_expert(&self) -> c_int {
1570        unsafe { llama_model_n_expert(self.model.as_ptr()) }
1571    }
1572
1573    /// Number of backend devices the model tensors are spread across.
1574    ///
1575    /// Use with [`Self::get_device`] to inspect each device. Returns `0` when
1576    /// the model is not yet loaded onto any device.
1577    #[must_use]
1578    pub fn n_devices(&self) -> c_int {
1579        unsafe { llama_model_n_devices(self.model.as_ptr()) }
1580    }
1581
1582    /// Get the backend device at `index`.
1583    ///
1584    /// Valid indices satisfy `0 <= index < n_devices()`. Returns `None` for
1585    /// out-of-range indices or when the device pointer is null.
1586    #[must_use]
1587    pub fn get_device(&self, index: i32) -> Option<LlamaBackendDevice> {
1588        if index < 0 || index >= self.n_devices() {
1589            return None;
1590        }
1591        let dev = unsafe { llama_model_get_device(self.model.as_ptr(), index) };
1592        if dev.is_null() {
1593            None
1594        } else {
1595            Some(LlamaBackendDevice { dev })
1596        }
1597    }
1598
1599    /// Iterate backend devices the model tensors are spread across.
1600    ///
1601    /// Equivalent to calling [`Self::get_device`] for `0..self.n_devices()`.
1602    /// Use [`LlamaBackendDevice::memory`] to inspect free/total bytes per device.
1603    #[must_use]
1604    pub fn devices(&self) -> LlamaBackendDevices<'_> {
1605        LlamaBackendDevices {
1606            model: self,
1607            next: 0,
1608        }
1609    }
1610
1611    /// Target-model layer indices stored in this checkpoint.
1612    ///
1613    /// Populated for EAGLE / distillation draft models that record which target
1614    /// layers they were trained against. Returns an empty slice when the
1615    /// metadata is absent.
1616    #[must_use]
1617    pub fn target_layer_ids(&self) -> &[i32] {
1618        let n = unsafe { llama_model_target_layer_ids_n(self.model.as_ptr()) };
1619        if n == 0 {
1620            return &[];
1621        }
1622        let ptr = unsafe { llama_model_target_layer_ids(self.model.as_ptr()) };
1623        if ptr.is_null() {
1624            &[]
1625        } else {
1626            unsafe { slice::from_raw_parts(ptr, n as usize) }
1627        }
1628    }
1629
1630    /// Get the number of attention heads in the model.
1631    #[must_use]
1632    pub fn n_head(&self) -> c_int {
1633        unsafe { llama_model_n_head(self.model.as_ptr()) }
1634    }
1635
1636    /// Get the number of key-value attention heads in the model.
1637    #[must_use]
1638    pub fn n_head_kv(&self) -> c_int {
1639        unsafe { llama_model_n_head_kv(self.model.as_ptr()) }
1640    }
1641
1642    /// Get the input embedding size of the model.
1643    #[must_use]
1644    pub fn n_embd_inp(&self) -> c_int {
1645        unsafe { llama_model_n_embd_inp(self.model.as_ptr()) }
1646    }
1647
1648    /// Get the output embedding size of the model.
1649    #[must_use]
1650    pub fn n_embd_out(&self) -> c_int {
1651        unsafe { llama_model_n_embd_out(self.model.as_ptr()) }
1652    }
1653
1654    /// Get the sliding window attention size of the model.
1655    /// Returns 0 if the model does not use sliding window attention.
1656    #[must_use]
1657    pub fn n_swa(&self) -> c_int {
1658        unsafe { llama_model_n_swa(self.model.as_ptr()) }
1659    }
1660
1661    /// Get the `RoPE` type used by the model.
1662    #[must_use]
1663    pub fn rope_type(&self) -> i32 {
1664        unsafe { llama_model_rope_type(self.model.as_ptr()) }
1665    }
1666
1667    /// Get the `RoPE` frequency scale used during training.
1668    #[must_use]
1669    pub fn rope_freq_scale_train(&self) -> f32 {
1670        unsafe { llama_model_rope_freq_scale_train(self.model.as_ptr()) }
1671    }
1672
1673    /// Get the model size in bytes.
1674    #[must_use]
1675    pub fn model_size(&self) -> u64 {
1676        unsafe { llama_model_size(self.model.as_ptr()) }
1677    }
1678
1679    /// Get the number of parameters in the model.
1680    #[must_use]
1681    pub fn n_params(&self) -> u64 {
1682        unsafe { llama_model_n_params(self.model.as_ptr()) }
1683    }
1684
1685    /// Get the number of classification outputs.
1686    #[must_use]
1687    pub fn n_cls_out(&self) -> u32 {
1688        unsafe { llama_model_n_cls_out(self.model.as_ptr()) }
1689    }
1690
1691    /// Get the classification label for the given index.
1692    ///
1693    /// # Errors
1694    ///
1695    /// Returns an error if the label is null or not valid UTF-8.
1696    pub fn cls_label(&self, index: u32) -> Result<&str, StringFromModelError> {
1697        let ptr = unsafe { llama_model_cls_label(self.model.as_ptr(), index) };
1698        if ptr.is_null() {
1699            return Err(StringFromModelError::ReturnedError(-1));
1700        }
1701        let cstr = unsafe { CStr::from_ptr(ptr) };
1702        cstr.to_str().map_err(StringFromModelError::Utf8Error)
1703    }
1704
1705    /// Adopt a raw `llama_model *`, taking ownership.
1706    ///
1707    /// # Safety
1708    ///
1709    /// `raw` must come from a llama.cpp entry point documented as returning a
1710    /// model the caller must release with `llama_model_free`, and must not be
1711    /// owned by anything else — [`Drop`] frees it.
1712    pub(crate) unsafe fn from_raw(raw: NonNull<llama_model>) -> Self {
1713        Self { model: raw }
1714    }
1715
1716    /// Get the number of metadata key-value pairs.
1717    #[must_use]
1718    pub fn meta_count(&self) -> c_int {
1719        unsafe { llama_model_meta_count(self.model.as_ptr()) }
1720    }
1721
1722    /// Get a chat template baked into the model, by name.
1723    ///
1724    /// Pass `None` for the default template (GGUF key `tokenizer.chat_template`),
1725    /// or a name to reach a variant — `Some("tool_use")` resolves
1726    /// `tokenizer.chat_template.tool_use`, which several models ship alongside
1727    /// their default and which is the one you want when the request carries
1728    /// tools.
1729    ///
1730    /// Supersedes the deprecated `get_chat_template`, which reads the default
1731    /// key straight out of GGUF metadata into a buffer the caller has to size —
1732    /// too small and it fails, too large and it wastes the allocation. This
1733    /// borrows a pointer llama.cpp already owns, so neither applies, and it can
1734    /// reach the named variants the other cannot.
1735    ///
1736    /// # Errors
1737    ///
1738    /// Returns [`StringFromModelError::ReturnedError`] if the model has no such
1739    /// template, or [`StringFromModelError::Utf8Error`] if it is not UTF-8.
1740    pub fn chat_template(&self, name: Option<&str>) -> Result<&str, StringFromModelError> {
1741        let c_name = match name {
1742            Some(n) => Some(CString::new(n).map_err(|_| StringFromModelError::ReturnedError(-1))?),
1743            None => None,
1744        };
1745        let name_ptr = c_name.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
1746        let ptr = unsafe { llama_model_chat_template(self.model.as_ptr(), name_ptr) };
1747        if ptr.is_null() {
1748            return Err(StringFromModelError::ReturnedError(-1));
1749        }
1750        let cstr = unsafe { CStr::from_ptr(ptr) };
1751        cstr.to_str().map_err(StringFromModelError::Utf8Error)
1752    }
1753
1754    /// Get the model's file type — the quantization it was stored with.
1755    ///
1756    /// Returns `None` for a type this crate's [`LlamaFtype`] does not know,
1757    /// which is what you get for `LLAMA_FTYPE_GUESSED` (the file did not
1758    /// specify one) or a type added upstream since this release.
1759    #[must_use]
1760    pub fn ftype(&self) -> Option<LlamaFtype> {
1761        let raw = unsafe { llama_model_ftype(self.model.as_ptr()) };
1762        LlamaFtype::try_from(raw).ok()
1763    }
1764
1765    /// Copy just the embedding rows for `tokens`, in order — `tokens.len() *
1766    /// n_embd` values, row-major.
1767    ///
1768    /// Prefer this over [`Self::token_embeddings`] whenever you know which
1769    /// tokens you want: that one materialises the entire matrix, which is
1770    /// `4 * n_vocab * n_embd` bytes — around 500 MB for a 128k-vocab 1024-dim
1771    /// model. Wraps `llama_model_get_tok_embd_rows`.
1772    ///
1773    /// # Errors
1774    ///
1775    /// Returns [`StringFromModelError::ReturnedError`] if the model exposes no
1776    /// token-embedding tensor, or if any token is out of range — llama.cpp
1777    /// reports both as a zero write without distinguishing them.
1778    ///
1779    /// # Examples
1780    ///
1781    /// ```no_run
1782    /// # use llama_cpp_4::model::LlamaModel;
1783    /// # use llama_cpp_4::token::LlamaToken;
1784    /// # fn f(model: &LlamaModel) -> Result<(), Box<dyn std::error::Error>> {
1785    /// let rows = model.token_embedding_rows(&[LlamaToken(0), LlamaToken(1)])?;
1786    /// assert_eq!(rows.len(), 2 * model.n_embd() as usize);
1787    /// # Ok(())
1788    /// # }
1789    /// ```
1790    pub fn token_embedding_rows(
1791        &self,
1792        tokens: &[LlamaToken],
1793    ) -> Result<Vec<f32>, StringFromModelError> {
1794        if tokens.is_empty() {
1795            return Ok(Vec::new());
1796        }
1797        let n_tokens =
1798            u32::try_from(tokens.len()).map_err(|_| StringFromModelError::ReturnedError(-1))?;
1799        let n_embd =
1800            usize::try_from(self.n_embd()).map_err(|_| StringFromModelError::ReturnedError(-1))?;
1801        let mut out = vec![0.0_f32; tokens.len() * n_embd];
1802
1803        // SAFETY: `tokens` is `#[repr(transparent)]` over `llama_token`, so the
1804        // cast is a layout no-op, and `n_tokens` is its real length. `out` is
1805        // sized `n_tokens * n_embd`, which is what upstream documents it writes.
1806        let written = unsafe {
1807            llama_cpp_sys_4::llama_model_get_tok_embd_rows(
1808                self.model.as_ptr(),
1809                tokens.as_ptr().cast(),
1810                n_tokens,
1811                out.as_mut_ptr(),
1812            )
1813        };
1814        if written == 0 {
1815            return Err(StringFromModelError::ReturnedError(-1));
1816        }
1817        out.truncate(written as usize);
1818        Ok(out)
1819    }
1820
1821    /// Labels a decision model attaches to its options, in order.
1822    ///
1823    /// Decision models (`clef`, `nimble`, `pplx-decider`, the `d1` family)
1824    /// score a list of options rather than generating text: you mark each
1825    /// option's span with
1826    /// [`LlamaBatchExt::set_decision_order`](crate::llama_batch_ext::LlamaBatchExt::set_decision_order),
1827    /// decode, and read one score per option out of the embeddings. These are
1828    /// the names those scores belong to — without them you have a vector of
1829    /// numbers and no way to say which option each is for.
1830    ///
1831    /// Empty for a model that is not a decision model. Wraps
1832    /// `llama_model_decision_label`, which reports
1833    /// `"<arch>.decision.labels"`.
1834    #[must_use]
1835    pub fn decision_labels(&self) -> Vec<String> {
1836        let mut out = Vec::new();
1837        for i in 0.. {
1838            // SAFETY: upstream returns null past the last label, which is the
1839            // loop's terminator; the pointer is a static C string owned by the
1840            // model, valid for as long as `&self`.
1841            let raw =
1842                unsafe { llama_cpp_sys_4::llama_model_decision_label(self.model.as_ptr(), i) };
1843            if raw.is_null() {
1844                break;
1845            }
1846            let label = unsafe { std::ffi::CStr::from_ptr(raw) };
1847            match label.to_str() {
1848                Ok(label) => out.push(label.to_owned()),
1849                // A non-UTF-8 label would be a broken GGUF; skipping it keeps
1850                // the indices of the rest aligned with the option order.
1851                Err(_) => out.push(String::new()),
1852            }
1853        }
1854        out
1855    }
1856
1857    /// How many tokens a realtime model's output lags behind its audio input.
1858    ///
1859    /// `0` when the model is not a realtime one. Used by mtmd to line generated
1860    /// audio up against the input stream; wraps `llama_model_n_tokens_delay`.
1861    #[must_use]
1862    pub fn n_tokens_delay(&self) -> u32 {
1863        // SAFETY: a plain read of model hparams through a live `&self`.
1864        unsafe { llama_cpp_sys_4::llama_model_n_tokens_delay(self.model.as_ptr()) }
1865    }
1866
1867    /// Turn the KV-cache attention rotation on or off for every context created
1868    /// from this model from now on.
1869    ///
1870    /// This is `TurboQuant`'s rotation, and upstream marks the setter as being
1871    /// for testing. Prefer
1872    /// [`LlamaContextParams::with_attn_rot_disabled`](crate::context::params::LlamaContextParams)
1873    /// for ordinary use — it scopes the choice to one context, where this
1874    /// mutates the model every later context inherits from. Wraps
1875    /// `llama_model_set_attn_rot`.
1876    pub fn set_attn_rot(&mut self, enabled: bool) {
1877        // SAFETY: `&mut self` is what rules out another context being created
1878        // from this model concurrently; the call only flips a model flag.
1879        unsafe { llama_cpp_sys_4::llama_model_set_attn_rot(self.model.as_ptr(), enabled) }
1880    }
1881
1882    /// Copy the whole token-embedding matrix out as `f32`, row-major and
1883    /// `n_vocab * n_embd` long.
1884    ///
1885    /// llama.cpp converts from whatever the tensor is stored as, so this
1886    /// allocates `4 * n_vocab * n_embd` bytes — 500 MB for a 128k-vocab 1024-dim
1887    /// model. Wraps `llama_model_get_tok_embd`.
1888    ///
1889    /// # Errors
1890    ///
1891    /// Returns [`StringFromModelError::ReturnedError`] if the model exposes no
1892    /// token-embedding tensor.
1893    pub fn token_embeddings(&self) -> Result<Vec<f32>, StringFromModelError> {
1894        // A null `out` asks for the element count without writing anything.
1895        let n = unsafe { llama_model_get_tok_embd(self.model.as_ptr(), std::ptr::null_mut()) };
1896        if n == 0 {
1897            return Err(StringFromModelError::ReturnedError(-1));
1898        }
1899        let mut out = vec![0.0_f32; n as usize];
1900        let written = unsafe { llama_model_get_tok_embd(self.model.as_ptr(), out.as_mut_ptr()) };
1901        if written == 0 {
1902            return Err(StringFromModelError::ReturnedError(-1));
1903        }
1904        // Trust the second call's count over the first: a shrink would otherwise
1905        // leave uninitialised-looking zeros on the tail.
1906        out.truncate(written as usize);
1907        Ok(out)
1908    }
1909
1910    /// Get a model description string.
1911    ///
1912    /// The `buf_size` parameter specifies the maximum buffer size for the description.
1913    /// A default of 256 bytes is usually sufficient.
1914    ///
1915    /// # Errors
1916    ///
1917    /// Returns an error if the description could not be retrieved or is not valid UTF-8.
1918    #[allow(clippy::cast_sign_loss)]
1919    pub fn desc(&self, buf_size: usize) -> Result<String, StringFromModelError> {
1920        let mut buf = vec![0u8; buf_size];
1921        let ret = unsafe {
1922            llama_model_desc(
1923                self.model.as_ptr(),
1924                buf.as_mut_ptr().cast::<c_char>(),
1925                buf_size,
1926            )
1927        };
1928        if ret < 0 {
1929            return Err(StringFromModelError::ReturnedError(ret));
1930        }
1931        let len = ret as usize;
1932        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1933        Ok(s.to_owned())
1934    }
1935
1936    /// Get a metadata key by index.
1937    ///
1938    /// The `buf_size` parameter specifies the maximum buffer size for the key.
1939    /// A default of 256 bytes is usually sufficient.
1940    ///
1941    /// # Errors
1942    ///
1943    /// Returns an error if the index is out of range or the key is not valid UTF-8.
1944    #[allow(clippy::cast_sign_loss)]
1945    pub fn meta_key_by_index(
1946        &self,
1947        index: i32,
1948        buf_size: usize,
1949    ) -> Result<String, StringFromModelError> {
1950        let mut buf = vec![0u8; buf_size];
1951        let ret = unsafe {
1952            llama_model_meta_key_by_index(
1953                self.model.as_ptr(),
1954                index,
1955                buf.as_mut_ptr().cast::<c_char>(),
1956                buf_size,
1957            )
1958        };
1959        if ret < 0 {
1960            return Err(StringFromModelError::ReturnedError(ret));
1961        }
1962        let len = ret as usize;
1963        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1964        Ok(s.to_owned())
1965    }
1966
1967    /// Get a metadata value string by index.
1968    ///
1969    /// The `buf_size` parameter specifies the maximum buffer size for the value.
1970    /// Values can be large (e.g. chat templates, token lists), so 4096+ may be needed.
1971    ///
1972    /// # Errors
1973    ///
1974    /// Returns an error if the index is out of range or the value is not valid UTF-8.
1975    #[allow(clippy::cast_sign_loss)]
1976    pub fn meta_val_str_by_index(
1977        &self,
1978        index: i32,
1979        buf_size: usize,
1980    ) -> Result<String, StringFromModelError> {
1981        let mut buf = vec![0u8; buf_size];
1982        let ret = unsafe {
1983            llama_model_meta_val_str_by_index(
1984                self.model.as_ptr(),
1985                index,
1986                buf.as_mut_ptr().cast::<c_char>(),
1987                buf_size,
1988            )
1989        };
1990        if ret < 0 {
1991            return Err(StringFromModelError::ReturnedError(ret));
1992        }
1993        let len = ret as usize;
1994        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1995        Ok(s.to_owned())
1996    }
1997
1998    /// Get a metadata value by key name.
1999    ///
2000    /// This is more convenient than iterating metadata by index when you know the key.
2001    /// The `buf_size` parameter specifies the maximum buffer size for the value.
2002    ///
2003    /// # Errors
2004    ///
2005    /// Returns an error if the key is not found, contains a null byte, or the value is not valid UTF-8.
2006    #[allow(clippy::cast_sign_loss)]
2007    pub fn meta_val_str(&self, key: &str, buf_size: usize) -> Result<String, StringFromModelError> {
2008        let c_key = CString::new(key).map_err(|_| StringFromModelError::ReturnedError(-1))?;
2009        let mut buf = vec![0u8; buf_size];
2010        let ret = unsafe {
2011            llama_model_meta_val_str(
2012                self.model.as_ptr(),
2013                c_key.as_ptr(),
2014                buf.as_mut_ptr().cast::<c_char>(),
2015                buf_size,
2016            )
2017        };
2018        if ret < 0 {
2019            return Err(StringFromModelError::ReturnedError(ret));
2020        }
2021        let len = ret as usize;
2022        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
2023        Ok(s.to_owned())
2024    }
2025
2026    /// Get all metadata as a list of `(key, value)` pairs.
2027    ///
2028    /// This is a convenience method that iterates over all metadata entries.
2029    /// Keys use a buffer of 256 bytes and values use 4096 bytes.
2030    /// For values that may be larger (e.g. token lists), use
2031    /// [`meta_val_str_by_index`](Self::meta_val_str_by_index) directly with a larger buffer.
2032    ///
2033    /// # Errors
2034    ///
2035    /// Returns an error if any key or value cannot be read or is not valid UTF-8.
2036    #[allow(clippy::cast_sign_loss)]
2037    pub fn metadata(&self) -> Result<Vec<(String, String)>, StringFromModelError> {
2038        let count = self.meta_count();
2039        let mut result = Vec::with_capacity(count as usize);
2040        for i in 0..count {
2041            let key = self.meta_key_by_index(i, 256)?;
2042            let val = self.meta_val_str_by_index(i, 4096)?;
2043            result.push((key, val));
2044        }
2045        Ok(result)
2046    }
2047
2048    /// Check if the model has an encoder.
2049    #[must_use]
2050    pub fn has_encoder(&self) -> bool {
2051        unsafe { llama_model_has_encoder(self.model.as_ptr()) }
2052    }
2053
2054    /// Check if the model has a decoder.
2055    #[must_use]
2056    pub fn has_decoder(&self) -> bool {
2057        unsafe { llama_model_has_decoder(self.model.as_ptr()) }
2058    }
2059
2060    /// Check if the model is recurrent (e.g. Mamba, RWKV).
2061    #[must_use]
2062    pub fn is_recurrent(&self) -> bool {
2063        unsafe { llama_model_is_recurrent(self.model.as_ptr()) }
2064    }
2065
2066    /// Check if the model is a hybrid model.
2067    #[must_use]
2068    pub fn is_hybrid(&self) -> bool {
2069        unsafe { llama_model_is_hybrid(self.model.as_ptr()) }
2070    }
2071
2072    /// Check if the model is a diffusion model.
2073    #[must_use]
2074    pub fn is_diffusion(&self) -> bool {
2075        unsafe { llama_model_is_diffusion(self.model.as_ptr()) }
2076    }
2077
2078    /// Get chat template from model.
2079    ///
2080    /// # Errors
2081    ///
2082    /// - If the model does not have a chat template, it will return an error.
2083    /// - If the chat template is not a valid `CString`, it will return an error.
2084    ///
2085    /// # Example
2086    ///
2087    /// ```no_run
2088    /// use llama_cpp_4::model::LlamaModel;
2089    /// use llama_cpp_4::model::params::LlamaModelParams;
2090    /// use llama_cpp_4::llama_backend::LlamaBackend;
2091    ///
2092    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2093    /// let backend = LlamaBackend::init()?;
2094    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2095    /// let chat_template = model.get_chat_template(1024)?;
2096    /// # Ok(())
2097    /// # }
2098    /// ```
2099    #[deprecated(
2100        since = "0.7.0",
2101        note = "use `chat_template(None)`, which borrows llama.cpp's own pointer \
2102                instead of copying into a caller-sized buffer, and can reach named \
2103                variants such as `chat_template(Some(\"tool_use\"))`"
2104    )]
2105    #[allow(clippy::missing_panics_doc)] // We statically know this will not panic as long as the buffer size is sufficient
2106    pub fn get_chat_template(&self, buf_size: usize) -> Result<String, ChatTemplateError> {
2107        // longest known template is about 1200 bytes from llama.cpp
2108        let chat_temp = CString::new(vec![b'*'; buf_size]).expect("no null");
2109        let chat_ptr = chat_temp.into_raw();
2110        let chat_name = CString::new("tokenizer.chat_template").expect("no null bytes");
2111
2112        let ret = unsafe {
2113            llama_model_meta_val_str(self.model.as_ptr(), chat_name.as_ptr(), chat_ptr, buf_size)
2114        };
2115
2116        if ret < 0 {
2117            return Err(ChatTemplateError::MissingTemplate(ret));
2118        }
2119
2120        let template_c = unsafe { CString::from_raw(chat_ptr) };
2121        let template = template_c.to_str()?;
2122
2123        let ret: usize = ret.try_into().unwrap();
2124        if template.len() < ret {
2125            return Err(ChatTemplateError::BuffSizeError(ret + 1));
2126        }
2127
2128        Ok(template.to_owned())
2129    }
2130
2131    /// Loads a model from a file.
2132    ///
2133    /// This function loads a model from a specified file path and returns the corresponding `LlamaModel` instance.
2134    ///
2135    /// # Errors
2136    ///
2137    /// - If the path cannot be converted to a string or if the model file does not exist, it will return an error.
2138    /// - If the model cannot be loaded (e.g., due to an invalid or corrupted model file), it will return a `LlamaModelLoadError`.
2139    ///
2140    /// # Example
2141    ///
2142    /// ```no_run
2143    /// use llama_cpp_4::model::LlamaModel;
2144    /// use llama_cpp_4::model::params::LlamaModelParams;
2145    /// use llama_cpp_4::llama_backend::LlamaBackend;
2146    ///
2147    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2148    /// let backend = LlamaBackend::init()?;
2149    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2150    /// # Ok(())
2151    /// # }
2152    /// ```
2153    #[tracing::instrument(skip_all, fields(params))]
2154    pub fn load_from_file(
2155        _: &LlamaBackend,
2156        path: impl AsRef<Path>,
2157        params: &LlamaModelParams,
2158    ) -> Result<Self, LlamaModelLoadError> {
2159        let path = path.as_ref();
2160        debug_assert!(
2161            Path::new(path).exists(),
2162            "{} does not exist",
2163            path.display()
2164        );
2165        let path = path
2166            .to_str()
2167            .ok_or(LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
2168
2169        let cstr = CString::new(path)?;
2170        let llama_model = unsafe { llama_model_load_from_file(cstr.as_ptr(), params.params) };
2171
2172        let model = NonNull::new(llama_model).ok_or(LlamaModelLoadError::NullResult)?;
2173
2174        tracing::debug!(?path, "Loaded model");
2175        Ok(LlamaModel { model })
2176    }
2177
2178    /// Load a model from multiple split files.
2179    ///
2180    /// This function loads a model that has been split across multiple files. This is useful for
2181    /// very large models that exceed filesystem limitations or need to be distributed across
2182    /// multiple storage devices.
2183    ///
2184    /// # Arguments
2185    ///
2186    /// * `paths` - A slice of paths to the split model files
2187    /// * `params` - The model parameters
2188    ///
2189    /// # Errors
2190    ///
2191    /// Returns an error if:
2192    /// - Any of the paths cannot be converted to a C string
2193    /// - The model fails to load from the splits
2194    /// - Any path doesn't exist or isn't accessible
2195    ///
2196    /// # Example
2197    ///
2198    /// ```no_run
2199    /// use llama_cpp_4::model::{LlamaModel, params::LlamaModelParams};
2200    /// use llama_cpp_4::llama_backend::LlamaBackend;
2201    ///
2202    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2203    /// let backend = LlamaBackend::init()?;
2204    /// let params = LlamaModelParams::default();
2205    ///
2206    /// let paths = vec![
2207    ///     "model-00001-of-00003.gguf",
2208    ///     "model-00002-of-00003.gguf",
2209    ///     "model-00003-of-00003.gguf",
2210    /// ];
2211    ///
2212    /// let model = LlamaModel::load_from_splits(&backend, &paths, &params)?;
2213    /// # Ok(())
2214    /// # }
2215    /// ```
2216    #[tracing::instrument(skip_all)]
2217    pub fn load_from_splits(
2218        _: &LlamaBackend,
2219        paths: &[impl AsRef<Path>],
2220        params: &LlamaModelParams,
2221    ) -> Result<Self, LlamaModelLoadError> {
2222        // Convert paths to C strings
2223        let c_strings: Vec<CString> = paths
2224            .iter()
2225            .map(|p| {
2226                let path = p.as_ref();
2227                debug_assert!(path.exists(), "{} does not exist", path.display());
2228                let path_str = path
2229                    .to_str()
2230                    .ok_or(LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
2231                CString::new(path_str).map_err(LlamaModelLoadError::from)
2232            })
2233            .collect::<Result<Vec<_>, _>>()?;
2234
2235        // Create array of pointers to C strings
2236        let c_ptrs: Vec<*const c_char> = c_strings.iter().map(|s| s.as_ptr()).collect();
2237
2238        // Load the model from splits
2239        let llama_model = unsafe {
2240            llama_model_load_from_splits(c_ptrs.as_ptr().cast_mut(), c_ptrs.len(), params.params)
2241        };
2242
2243        let model = NonNull::new(llama_model).ok_or(LlamaModelLoadError::NullResult)?;
2244
2245        tracing::debug!("Loaded model from {} splits", paths.len());
2246        Ok(LlamaModel { model })
2247    }
2248
2249    /// Load a model from a `FILE` pointer.
2250    ///
2251    /// # Safety
2252    ///
2253    /// The `file` pointer must be a valid, open `FILE*`.
2254    ///
2255    /// # Errors
2256    ///
2257    /// Returns an error if the model cannot be loaded.
2258    pub unsafe fn load_from_file_ptr(
2259        file: *mut llama_cpp_sys_4::FILE,
2260        params: &LlamaModelParams,
2261    ) -> Result<Self, LlamaModelLoadError> {
2262        let model = llama_cpp_sys_4::llama_model_load_from_file_ptr(file, params.params);
2263        let model = NonNull::new(model).ok_or(LlamaModelLoadError::NullResult)?;
2264        Ok(LlamaModel { model })
2265    }
2266
2267    /// Initialize a model from user-provided data.
2268    ///
2269    /// # Safety
2270    ///
2271    /// The metadata, callback, and user data must be valid.
2272    ///
2273    /// # Errors
2274    ///
2275    /// Returns an error if the model cannot be initialized.
2276    pub unsafe fn init_from_user(
2277        metadata: *mut llama_cpp_sys_4::gguf_context,
2278        set_tensor_data: llama_cpp_sys_4::llama_model_set_tensor_data_t,
2279        set_tensor_data_ud: *mut std::ffi::c_void,
2280        params: &LlamaModelParams,
2281    ) -> Result<Self, LlamaModelLoadError> {
2282        let model = llama_cpp_sys_4::llama_model_init_from_user(
2283            metadata,
2284            set_tensor_data,
2285            set_tensor_data_ud,
2286            params.params,
2287        );
2288        let model = NonNull::new(model).ok_or(LlamaModelLoadError::NullResult)?;
2289        Ok(LlamaModel { model })
2290    }
2291
2292    /// Save the model to a file.
2293    ///
2294    /// # Panics
2295    ///
2296    /// Panics if the path contains null bytes.
2297    pub fn save_to_file(&self, path: impl AsRef<Path>) {
2298        let path = path.as_ref();
2299        let path_str = path.to_str().expect("path is not valid UTF-8");
2300        let c_path = CString::new(path_str).expect("path contains null bytes");
2301        unsafe {
2302            llama_model_save_to_file(self.model.as_ptr(), c_path.as_ptr());
2303        }
2304    }
2305
2306    /// Get the list of built-in chat templates.
2307    ///
2308    /// Returns the names of all chat templates that are built into llama.cpp.
2309    ///
2310    /// # Panics
2311    ///
2312    /// Panics if any template name is not valid UTF-8.
2313    #[allow(clippy::cast_sign_loss)]
2314    #[must_use]
2315    pub fn chat_builtin_templates() -> Vec<String> {
2316        // First call to get count
2317        let count = unsafe { llama_chat_builtin_templates(std::ptr::null_mut(), 0) };
2318        if count <= 0 {
2319            return Vec::new();
2320        }
2321        let count = count as usize;
2322        let mut ptrs: Vec<*const c_char> = vec![std::ptr::null(); count];
2323        unsafe {
2324            llama_chat_builtin_templates(ptrs.as_mut_ptr(), count);
2325        }
2326        ptrs.iter()
2327            .map(|&p| {
2328                let cstr = unsafe { CStr::from_ptr(p) };
2329                cstr.to_str()
2330                    .expect("template name is not valid UTF-8")
2331                    .to_owned()
2332            })
2333            .collect()
2334    }
2335
2336    /// Initializes a lora adapter from a file.
2337    ///
2338    /// This function initializes a Lora adapter, which is a model extension used to adapt or fine-tune the existing model
2339    /// to a specific domain or task. The adapter file is typically in the form of a binary or serialized file that can be applied
2340    /// to the model for improved performance on specialized tasks.
2341    ///
2342    /// # Errors
2343    ///
2344    /// - If the adapter file path cannot be converted to a string or if the adapter cannot be initialized, it will return an error.
2345    ///
2346    /// # Example
2347    ///
2348    /// ```no_run
2349    /// use llama_cpp_4::model::LlamaModel;
2350    /// use llama_cpp_4::model::params::LlamaModelParams;
2351    /// use llama_cpp_4::llama_backend::LlamaBackend;
2352    ///
2353    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2354    /// let backend = LlamaBackend::init()?;
2355    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2356    /// let adapter = model.lora_adapter_init("path/to/lora/adapter")?;
2357    /// # Ok(())
2358    /// # }
2359    /// ```
2360    pub fn lora_adapter_init(
2361        &self,
2362        path: impl AsRef<Path>,
2363    ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
2364        let path = path.as_ref();
2365        debug_assert!(
2366            Path::new(path).exists(),
2367            "{} does not exist",
2368            path.display()
2369        );
2370
2371        let path = path
2372            .to_str()
2373            .ok_or(LlamaLoraAdapterInitError::PathToStrError(
2374                path.to_path_buf(),
2375            ))?;
2376
2377        let cstr = CString::new(path)?;
2378        let adapter = unsafe { llama_adapter_lora_init(self.model.as_ptr(), cstr.as_ptr()) };
2379
2380        let adapter = NonNull::new(adapter).ok_or(LlamaLoraAdapterInitError::NullResult)?;
2381
2382        tracing::debug!(?path, "Initialized lora adapter");
2383        Ok(LlamaLoraAdapter {
2384            lora_adapter: adapter,
2385        })
2386    }
2387
2388    /// Initialize a `LoRA` adapter from an open `FILE` pointer, reading from its
2389    /// current position.
2390    ///
2391    /// Wraps `llama_adapter_lora_init_from_file_ptr`
2392    /// ([llama.cpp #28993](https://github.com/ggml-org/llama.cpp/pull/28993)),
2393    /// the counterpart to
2394    /// [`LlamaModel::load_from_file_ptr`]. Use it when the adapter GGUF is
2395    /// *embedded* in a larger file — appended to an executable, or packed into
2396    /// an archive — which is the one thing
2397    /// [`Self::lora_adapter_init`] cannot do, since it opens the path itself
2398    /// and reads from byte zero. For an adapter that is its own file, use that
2399    /// instead.
2400    ///
2401    /// The adapter does not keep the `FILE`: llama.cpp reads what it needs
2402    /// before returning, exactly as `llama_adapter_lora_init` does before it
2403    /// `fclose`s its own handle. So the caller still owns the handle and should
2404    /// close it, and the returned [`LlamaLoraAdapter`] stays valid afterwards.
2405    ///
2406    /// # Safety
2407    ///
2408    /// `file` must be a valid, open `FILE*` positioned at the start of the
2409    /// adapter GGUF. Reading advances the position and llama.cpp does not
2410    /// restore it.
2411    ///
2412    /// # Errors
2413    ///
2414    /// Returns [`LlamaLoraAdapterInitError::NullResult`] if `file` is null or
2415    /// llama.cpp could not read an adapter from it; the reason is in the
2416    /// llama.cpp log.
2417    pub unsafe fn lora_adapter_init_from_file_ptr(
2418        &self,
2419        file: *mut llama_cpp_sys_4::FILE,
2420    ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
2421        let adapter =
2422            llama_cpp_sys_4::llama_adapter_lora_init_from_file_ptr(self.model.as_ptr(), file);
2423        let adapter = NonNull::new(adapter).ok_or(LlamaLoraAdapterInitError::NullResult)?;
2424
2425        tracing::debug!("Initialized lora adapter from a FILE pointer");
2426        Ok(LlamaLoraAdapter {
2427            lora_adapter: adapter,
2428        })
2429    }
2430
2431    /// Initialize a `LoRA` adapter from an adapter GGUF embedded at `offset`
2432    /// inside a larger file.
2433    ///
2434    /// [`Self::lora_adapter_init`] opens the path itself and reads from byte
2435    /// zero, so it only works for an adapter that is its own file. This is the
2436    /// safe way to reach llama.cpp's `FILE *`-based loader for the other case:
2437    /// an adapter appended to an executable, packed into an archive, or stored
2438    /// alongside other blobs. Pass `0` and it behaves like
2439    /// [`Self::lora_adapter_init`].
2440    ///
2441    /// The handle is opened, positioned, read and closed inside this call;
2442    /// llama.cpp does not keep it.
2443    ///
2444    /// # Examples
2445    ///
2446    /// ```no_run
2447    /// # use llama_cpp_4::model::LlamaModel;
2448    /// # fn f(model: &LlamaModel) -> Result<(), Box<dyn std::error::Error>> {
2449    /// // The adapter starts 4 KiB into a bundle of several.
2450    /// let adapter = model.lora_adapter_init_at_offset("bundle.bin", 4096)?;
2451    /// # drop(adapter);
2452    /// # Ok(())
2453    /// # }
2454    /// ```
2455    ///
2456    /// # Errors
2457    ///
2458    /// - [`LlamaLoraAdapterInitError::FileOpen`] if the file cannot be opened.
2459    /// - [`LlamaLoraAdapterInitError::FileSeek`] if it cannot be positioned at
2460    ///   `offset` — including an `offset` too large for a 32-bit target's file
2461    ///   offsets.
2462    /// - [`LlamaLoraAdapterInitError::NullResult`] if llama.cpp could not read
2463    ///   an adapter from that position; the reason is in the llama.cpp log.
2464    pub fn lora_adapter_init_at_offset(
2465        &self,
2466        path: impl AsRef<Path>,
2467        offset: u64,
2468    ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
2469        let path = path.as_ref();
2470
2471        let mut file = crate::cfile::CFile::open_read(path).map_err(|e| {
2472            LlamaLoraAdapterInitError::FileOpen {
2473                path: path.to_path_buf(),
2474                reason: e.to_string(),
2475            }
2476        })?;
2477        file.seek_to(offset)
2478            .map_err(|e| LlamaLoraAdapterInitError::FileSeek {
2479                path: path.to_path_buf(),
2480                offset,
2481                reason: e.to_string(),
2482            })?;
2483
2484        // SAFETY: `file` is open for the duration of the call, and llama.cpp
2485        // does not retain it — it reads what it needs before returning, just as
2486        // `llama_adapter_lora_init` does before closing its own handle.
2487        let adapter = unsafe { self.lora_adapter_init_from_file_ptr(file.as_ptr()) }?;
2488
2489        tracing::debug!(?path, offset, "Initialized lora adapter at offset");
2490        Ok(adapter)
2491    }
2492
2493    /// Create a new context from this model.
2494    ///
2495    /// This function creates a new context for the model, which is used to manage and perform computations for inference,
2496    /// including token generation, embeddings, and other tasks that the model can perform. The context allows fine-grained
2497    /// control over model parameters for a specific task.
2498    ///
2499    /// # Errors
2500    ///
2501    /// - There are various potential failures such as invalid parameters or a failure to allocate the context. See [`LlamaContextLoadError`]
2502    ///   for more detailed error descriptions.
2503    ///
2504    /// # Example
2505    ///
2506    /// ```no_run
2507    /// use llama_cpp_4::model::LlamaModel;
2508    /// use llama_cpp_4::model::params::LlamaModelParams;
2509    /// use llama_cpp_4::context::params::LlamaContextParams;
2510    /// use llama_cpp_4::llama_backend::LlamaBackend;
2511    ///
2512    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2513    /// let backend = LlamaBackend::init()?;
2514    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2515    /// let context = model.new_context(&backend, LlamaContextParams::default())?;
2516    /// # Ok(())
2517    /// # }
2518    /// ```
2519    #[allow(clippy::needless_pass_by_value)]
2520    pub fn new_context(
2521        &self,
2522        _: &LlamaBackend,
2523        mut params: LlamaContextParams,
2524    ) -> Result<LlamaContext<'_>, LlamaContextLoadError> {
2525        // Apply TurboQuant attn-rotation preference before the KV cache is
2526        // initialised inside llama_init_from_model.
2527        let prev_rot_var = std::env::var("LLAMA_ATTN_ROT_DISABLE").ok();
2528        if params.attn_rot_disabled {
2529            // SAFETY: we restore the value right after the call.
2530            #[allow(unused_unsafe)]
2531            unsafe {
2532                std::env::set_var("LLAMA_ATTN_ROT_DISABLE", "1");
2533            }
2534        } else if std::env::var("LLAMA_ATTN_ROT_DISABLE").is_ok() {
2535            // params say "enabled" – only clear if it was previously unset
2536            // (respect explicit user env var).
2537        }
2538
2539        let context_type = params.ctx_type();
2540        let context_params = params.context_params;
2541        let embeddings_enabled = params.embeddings();
2542        let context = unsafe { llama_init_from_model(self.model.as_ptr(), context_params) };
2543
2544        // Restore the env-var to its previous state.
2545        #[allow(unused_unsafe)]
2546        match prev_rot_var {
2547            Some(v) => unsafe { std::env::set_var("LLAMA_ATTN_ROT_DISABLE", v) },
2548            None if params.attn_rot_disabled => unsafe {
2549                std::env::remove_var("LLAMA_ATTN_ROT_DISABLE");
2550            },
2551            None => {}
2552        }
2553
2554        let context = NonNull::new(context).ok_or(LlamaContextLoadError::NullReturn)?;
2555        let tensor_transactions = params.tensor_transactions.take();
2556        Ok(LlamaContext::new(
2557            self,
2558            context,
2559            embeddings_enabled,
2560            context_type,
2561            tensor_transactions,
2562        ))
2563    }
2564
2565    /// Apply the model's chat template to a sequence of messages.
2566    ///
2567    /// This function applies the model's chat template to the provided chat messages, formatting them accordingly. The chat
2568    /// template determines the structure or style of conversation between the system and user, such as token formatting,
2569    /// role separation, and more. The template can be customized by providing an optional template string, or if `None`
2570    /// is provided, the default template used by `llama.cpp` will be applied.
2571    ///
2572    /// For more information on supported templates, visit:
2573    /// <https://github.com/ggerganov/llama.cpp/wiki/Templates-supported-by-llama_chat_apply_template>
2574    ///
2575    /// # Arguments
2576    ///
2577    /// - `tmpl`: An optional custom template string. If `None`, the default template will be used.
2578    /// - `chat`: A vector of `LlamaChatMessage` instances, which represent the conversation between the system and user.
2579    /// - `add_ass`: A boolean flag indicating whether additional system-specific instructions (like "assistant") should be included.
2580    ///
2581    /// # Errors
2582    ///
2583    /// There are several possible points of failure when applying the chat template:
2584    /// - Insufficient buffer size to hold the formatted chat (this will return `ApplyChatTemplateError::BuffSizeError`).
2585    /// - If the template or messages cannot be processed properly, various errors from `ApplyChatTemplateError` may occur.
2586    ///
2587    /// # Example
2588    ///
2589    /// ```no_run
2590    /// use llama_cpp_4::model::{LlamaModel, LlamaChatMessage};
2591    /// use llama_cpp_4::model::params::LlamaModelParams;
2592    /// use llama_cpp_4::llama_backend::LlamaBackend;
2593    ///
2594    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2595    /// let backend = LlamaBackend::init()?;
2596    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2597    /// let chat = vec![
2598    ///     LlamaChatMessage::new("user".to_string(), "Hello!".to_string())?,
2599    ///     LlamaChatMessage::new("assistant".to_string(), "Hi! How can I assist you today?".to_string())?,
2600    /// ];
2601    /// let formatted_chat = model.apply_chat_template(None, &chat, true)?;
2602    /// # Ok(())
2603    /// # }
2604    /// ```
2605    ///
2606    /// # Notes
2607    ///
2608    /// The provided buffer is twice the length of the messages by default, which is recommended by the `llama.cpp` documentation.
2609    /// # Panics
2610    ///
2611    /// Panics if the buffer length exceeds `i32::MAX`.
2612    #[tracing::instrument(skip_all)]
2613    pub fn apply_chat_template(
2614        &self,
2615        tmpl: Option<&str>,
2616        chat: &[LlamaChatMessage],
2617        add_ass: bool,
2618    ) -> Result<String, ApplyChatTemplateError> {
2619        // Compute raw message byte total from the original LlamaChatMessage vec
2620        // *before* we shadow `chat` with the sys-type vec below.
2621        let message_length = chat.iter().fold(0usize, |acc, c| {
2622            acc + c.role.to_bytes().len() + c.content.to_bytes().len()
2623        });
2624
2625        // Build our llama_cpp_sys chat messages (raw pointers into CStrings).
2626        let chat_sys: Vec<llama_chat_message> = chat
2627            .iter()
2628            .map(|c| llama_chat_message {
2629                role: c.role.as_ptr(),
2630                content: c.content.as_ptr(),
2631            })
2632            .collect();
2633
2634        // Set the tmpl pointer.
2635        let tmpl_cstring = tmpl.map(CString::new).transpose()?;
2636        let tmpl_ptr = tmpl_cstring
2637            .as_ref()
2638            .map_or(std::ptr::null(), |s| s.as_ptr());
2639
2640        // `message_length * 4` is far too small for models whose built-in chat
2641        // template adds a long default system prompt (e.g. Qwen3.5 prepends
2642        // ~80+ chars of markup even for a one-word user message).  Start with
2643        // at least 4 KiB so short inputs like "hi" always have room.
2644        //
2645        // `llama_chat_apply_template` returns the number of bytes it *actually*
2646        // needed when the buffer was too small, so we retry exactly once with
2647        // that precise size rather than giving up immediately.
2648        let mut buf_size = message_length.saturating_mul(4).max(4096);
2649
2650        for _ in 0..2 {
2651            // Use u8 so that as_mut_ptr()/as_ptr() match the binding (*mut u8 / *const u8).
2652            let mut buff = vec![0u8; buf_size];
2653            let res = unsafe {
2654                llama_chat_apply_template(
2655                    tmpl_ptr,
2656                    chat_sys.as_ptr(),
2657                    chat_sys.len(),
2658                    add_ass,
2659                    buff.as_mut_ptr().cast(),
2660                    i32::try_from(buff.len()).expect("buffer length fits in i32"),
2661                )
2662            };
2663
2664            if res < 0 {
2665                return Err(ApplyChatTemplateError::BuffSizeError);
2666            }
2667
2668            #[allow(clippy::cast_sign_loss)]
2669            let needed = res as usize;
2670            if needed > buf_size {
2671                // Buffer was too small — retry with the exact size llama.cpp reported.
2672                buf_size = needed + 1; // +1 for null terminator
2673                continue;
2674            }
2675
2676            // SAFETY: llama_chat_apply_template wrote a NUL-terminated string
2677            // into `buff`; `needed` bytes were used.
2678            let formatted = unsafe {
2679                CStr::from_ptr(buff.as_ptr().cast())
2680                    .to_string_lossy()
2681                    .into_owned()
2682            };
2683            return Ok(formatted);
2684        }
2685
2686        Err(ApplyChatTemplateError::BuffSizeError)
2687    }
2688
2689    /// Build a split GGUF file path for a specific chunk.
2690    ///
2691    /// This utility function creates the standardized filename for a split model chunk
2692    /// following the pattern: `{prefix}-{split_no:05d}-of-{split_count:05d}.gguf`
2693    ///
2694    /// # Arguments
2695    ///
2696    /// * `path_prefix` - The base path and filename prefix
2697    /// * `split_no` - The split number (1-indexed)
2698    /// * `split_count` - The total number of splits
2699    ///
2700    /// # Returns
2701    ///
2702    /// Returns the formatted split path as a String
2703    ///
2704    /// # Example
2705    ///
2706    /// ```
2707    /// use llama_cpp_4::model::LlamaModel;
2708    ///
2709    /// let path = LlamaModel::split_path("/models/llama", 1, 4);
2710    /// assert_eq!(path, "/models/llama-00002-of-00004.gguf");
2711    /// ```
2712    ///
2713    /// # Panics
2714    ///
2715    /// Panics if the path prefix contains a null byte.
2716    #[must_use]
2717    pub fn split_path(path_prefix: &str, split_no: i32, split_count: i32) -> String {
2718        let mut buffer = vec![0u8; 1024];
2719        let len = unsafe {
2720            llama_split_path(
2721                buffer.as_mut_ptr().cast::<c_char>(),
2722                buffer.len(),
2723                CString::new(path_prefix).unwrap().as_ptr(),
2724                split_no,
2725                split_count,
2726            )
2727        };
2728
2729        let len = usize::try_from(len).expect("split_path length fits in usize");
2730        buffer.truncate(len);
2731        String::from_utf8(buffer).unwrap_or_default()
2732    }
2733
2734    /// Extract the path prefix from a split filename.
2735    ///
2736    /// This function extracts the base path prefix from a split model filename,
2737    /// but only if the `split_no` and `split_count` match the pattern in the filename.
2738    ///
2739    /// # Arguments
2740    ///
2741    /// * `split_path` - The full path to the split file
2742    /// * `split_no` - The expected split number
2743    /// * `split_count` - The expected total number of splits
2744    ///
2745    /// # Returns
2746    ///
2747    /// Returns the path prefix if the pattern matches, or None if it doesn't
2748    ///
2749    /// # Example
2750    ///
2751    /// ```
2752    /// use llama_cpp_4::model::LlamaModel;
2753    ///
2754    /// let prefix = LlamaModel::split_prefix("/models/llama-00002-of-00004.gguf", 1, 4);
2755    /// assert_eq!(prefix, Some("/models/llama".to_string()));
2756    /// ```
2757    ///
2758    /// # Panics
2759    ///
2760    /// Panics if the split path contains a null byte.
2761    #[must_use]
2762    pub fn split_prefix(split_path: &str, split_no: i32, split_count: i32) -> Option<String> {
2763        let mut buffer = vec![0u8; 1024];
2764        let len = unsafe {
2765            llama_split_prefix(
2766                buffer.as_mut_ptr().cast::<c_char>(),
2767                buffer.len(),
2768                CString::new(split_path).unwrap().as_ptr(),
2769                split_no,
2770                split_count,
2771            )
2772        };
2773
2774        if len > 0 {
2775            let len = usize::try_from(len).expect("split_prefix length fits in usize");
2776            buffer.truncate(len);
2777            String::from_utf8(buffer).ok()
2778        } else {
2779            None
2780        }
2781    }
2782}
2783
2784#[allow(clippy::cast_precision_loss)]
2785impl fmt::Display for LlamaModel {
2786    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2787        let desc = self.desc(256).unwrap_or_else(|_| "unknown".to_string());
2788        write!(
2789            f,
2790            "{desc} | {layers}L {heads}H {embd}E | {params} params | {size:.1} MiB",
2791            layers = self.n_layer(),
2792            heads = self.n_head(),
2793            embd = self.n_embd(),
2794            params = self.n_params(),
2795            size = self.model_size() as f64 / (1024.0 * 1024.0),
2796        )
2797    }
2798}
2799
2800impl Drop for LlamaModel {
2801    fn drop(&mut self) {
2802        unsafe { llama_model_free(self.model.as_ptr()) }
2803    }
2804}
2805
2806/// Defines the possible types of vocabulary used by the model.
2807///
2808/// The model may use different types of vocabulary depending on the tokenization method chosen during training.
2809/// This enum represents these types, specifically `BPE` (Byte Pair Encoding) and `SPM` (`SentencePiece`).
2810///
2811/// # Variants
2812///
2813/// - `BPE`: Byte Pair Encoding, a common tokenization method used in NLP tasks.
2814/// - `SPM`: `SentencePiece`, another popular tokenization method for NLP models.
2815///
2816/// # Example
2817///
2818/// ```no_run
2819/// use llama_cpp_4::model::VocabType;
2820///
2821/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2822/// let vocab_type = VocabType::BPE;
2823/// match vocab_type {
2824///     VocabType::BPE => println!("The model uses Byte Pair Encoding (BPE)"),
2825///     VocabType::SPM => println!("The model uses SentencePiece (SPM)"),
2826/// }
2827/// # Ok(())
2828/// # }
2829/// ```
2830#[repr(u32)]
2831#[derive(Debug, Eq, Copy, Clone, PartialEq)]
2832pub enum VocabType {
2833    /// Byte Pair Encoding
2834    BPE = LLAMA_VOCAB_TYPE_BPE as _,
2835    /// Sentence Piece Tokenizer
2836    SPM = LLAMA_VOCAB_TYPE_SPM as _,
2837}
2838
2839/// Error that occurs when trying to convert a `llama_vocab_type` to a `VocabType`.
2840///
2841/// This error is raised when the integer value returned by the system does not correspond to a known vocabulary type.
2842///
2843/// # Variants
2844///
2845/// - `UnknownValue`: The error is raised when the value is not a valid `llama_vocab_type`. The invalid value is returned with the error.
2846///
2847/// # Example
2848///
2849/// ```no_run
2850/// use llama_cpp_4::model::LlamaTokenTypeFromIntError;
2851///
2852/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2853/// let invalid_value = 999; // Not a valid vocabulary type
2854/// let error = LlamaTokenTypeFromIntError::UnknownValue(invalid_value);
2855/// println!("Error: {}", error);
2856/// # Ok(())
2857/// # }
2858/// ```
2859#[derive(thiserror::Error, Debug, Eq, PartialEq)]
2860pub enum LlamaTokenTypeFromIntError {
2861    /// The value is not a valid `llama_token_type`. Contains the int value that was invalid.
2862    #[error("Unknown Value {0}")]
2863    UnknownValue(llama_vocab_type),
2864}
2865
2866impl TryFrom<llama_vocab_type> for VocabType {
2867    type Error = LlamaTokenTypeFromIntError;
2868
2869    fn try_from(value: llama_vocab_type) -> Result<Self, Self::Error> {
2870        match value {
2871            LLAMA_VOCAB_TYPE_BPE => Ok(VocabType::BPE),
2872            LLAMA_VOCAB_TYPE_SPM => Ok(VocabType::SPM),
2873            unknown => Err(LlamaTokenTypeFromIntError::UnknownValue(unknown)),
2874        }
2875    }
2876}