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        let add_bos = match add_bos {
1044            AddBos::Always => true,
1045            AddBos::Never => false,
1046        };
1047        if let Some(position) = str.as_bytes().iter().position(|byte| *byte == 0) {
1048            return Err(StringToTokenError::InteriorNul(position));
1049        }
1050        let tokens_estimation = std::cmp::max(8, (str.len() / 2) + usize::from(add_bos));
1051        buffer.clear();
1052        if buffer.capacity() < tokens_estimation {
1053            buffer.reserve(tokens_estimation);
1054        }
1055        let buffer_capacity = c_int::try_from(buffer.capacity())?;
1056        let text_length = c_int::try_from(str.len())?;
1057        let size = unsafe {
1058            llama_tokenize(
1059                self.get_vocab().vocab.as_ref(),
1060                str.as_ptr().cast(),
1061                text_length,
1062                buffer.as_mut_ptr().cast(),
1063                buffer_capacity,
1064                add_bos,
1065                true,
1066            )
1067        };
1068
1069        // if we fail the first time we can resize the vector to the correct size and try again. This should never fail.
1070        // as a result - size is guaranteed to be positive here.
1071        let size = if size.is_negative() {
1072            let required = usize::try_from(size.unsigned_abs())?;
1073            if buffer.capacity() < required {
1074                buffer.reserve_exact(required);
1075            }
1076            let retry_capacity = c_int::try_from(buffer.capacity())?;
1077            unsafe {
1078                llama_tokenize(
1079                    self.get_vocab().vocab.as_ref(),
1080                    str.as_ptr().cast(),
1081                    text_length,
1082                    buffer.as_mut_ptr().cast(),
1083                    retry_capacity,
1084                    add_bos,
1085                    true,
1086                )
1087            }
1088        } else {
1089            size
1090        };
1091
1092        let native_size = size;
1093        let size = usize::try_from(native_size)?;
1094        if size > buffer.capacity() {
1095            return Err(StringToTokenError::NativeTokenCount {
1096                returned: native_size,
1097                capacity: buffer.capacity(),
1098            });
1099        }
1100
1101        // Safety: `size <= capacity` and llama.cpp initialized elements up to `size`.
1102        unsafe { buffer.set_len(size) }
1103        Ok(())
1104    }
1105
1106    /// Counts tokens without materializing token identifiers.
1107    ///
1108    /// # Errors
1109    ///
1110    /// Returns an error when the text contains NUL or its length/count cannot
1111    /// be represented by the native API.
1112    pub fn str_token_count(&self, str: &str, add_bos: AddBos) -> Result<usize, StringToTokenError> {
1113        let add_bos = matches!(add_bos, AddBos::Always);
1114        if let Some(position) = str.as_bytes().iter().position(|byte| *byte == 0) {
1115            return Err(StringToTokenError::InteriorNul(position));
1116        }
1117        let size = unsafe {
1118            llama_tokenize(
1119                self.get_vocab().vocab.as_ref(),
1120                str.as_ptr().cast(),
1121                c_int::try_from(str.len())?,
1122                std::ptr::null_mut(),
1123                0,
1124                add_bos,
1125                true,
1126            )
1127        };
1128        Ok(usize::try_from(i64::from(size).unsigned_abs())?)
1129    }
1130
1131    /// Get the type of a token.
1132    ///
1133    /// This function retrieves the attributes associated with a given token. The attributes are typically used to
1134    /// understand whether the token represents a special type of token (e.g., beginning-of-sequence (BOS), end-of-sequence (EOS),
1135    /// control tokens, etc.).
1136    ///
1137    /// # Panics
1138    ///
1139    /// - This function will panic if the token type is unknown or cannot be converted to a valid `LlamaTokenAttrs`.
1140    ///
1141    /// # Example
1142    ///
1143    /// ```no_run
1144    /// use llama_cpp_4::model::LlamaModel;
1145    /// use llama_cpp_4::model::params::LlamaModelParams;
1146    /// use llama_cpp_4::llama_backend::LlamaBackend;
1147    /// use llama_cpp_4::token::LlamaToken;
1148    ///
1149    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1150    /// let backend = LlamaBackend::init()?;
1151    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1152    /// let token = LlamaToken::new(42);
1153    /// let token_attrs = model.token_attr(token);
1154    /// # Ok(())
1155    /// # }
1156    /// ```
1157    #[must_use]
1158    pub fn token_attr(&self, LlamaToken(id): LlamaToken) -> LlamaTokenAttrs {
1159        let token_type =
1160            unsafe { llama_cpp_sys_4::llama_vocab_get_attr(self.get_vocab().vocab.as_ref(), id) };
1161        LlamaTokenAttrs::try_from(token_type).expect("token type is valid")
1162    }
1163
1164    /// Detokenize a slice of tokens into a string.
1165    ///
1166    /// This is the inverse of [`str_to_token`](Self::str_to_token).
1167    ///
1168    /// # Parameters
1169    ///
1170    /// - `tokens`: The tokens to detokenize.
1171    /// - `remove_special`: If `true`, special tokens are removed from the output.
1172    /// - `unparse_special`: If `true`, special tokens are rendered as their text representation.
1173    ///
1174    /// # Errors
1175    ///
1176    /// Returns an error if the detokenized text is not valid UTF-8.
1177    #[allow(
1178        clippy::cast_possible_truncation,
1179        clippy::cast_possible_wrap,
1180        clippy::cast_sign_loss
1181    )]
1182    pub fn detokenize(
1183        &self,
1184        tokens: &[LlamaToken],
1185        remove_special: bool,
1186        unparse_special: bool,
1187    ) -> Result<String, StringFromModelError> {
1188        // First call with empty buffer to get required size
1189        let n_tokens = tokens.len() as i32;
1190        let token_ptr = tokens.as_ptr().cast::<llama_cpp_sys_4::llama_token>();
1191        let needed = unsafe {
1192            llama_detokenize(
1193                self.get_vocab().vocab.as_ref(),
1194                token_ptr,
1195                n_tokens,
1196                std::ptr::null_mut(),
1197                0,
1198                remove_special,
1199                unparse_special,
1200            )
1201        };
1202        // llama_detokenize returns negative required size when buffer is too small
1203        let buf_size = if needed < 0 {
1204            (-needed) as usize
1205        } else {
1206            needed as usize
1207        };
1208        let mut buf = vec![0u8; buf_size];
1209        let ret = unsafe {
1210            llama_detokenize(
1211                self.get_vocab().vocab.as_ref(),
1212                token_ptr,
1213                n_tokens,
1214                buf.as_mut_ptr().cast::<c_char>(),
1215                buf_size as i32,
1216                remove_special,
1217                unparse_special,
1218            )
1219        };
1220        if ret < 0 {
1221            return Err(StringFromModelError::ReturnedError(ret));
1222        }
1223        let len = ret as usize;
1224        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1225        Ok(s.to_owned())
1226    }
1227
1228    /// Convert a token to a string with a specified buffer size.
1229    ///
1230    /// This function allows you to convert a token into a string, with the ability to specify a buffer size for the operation.
1231    /// It is generally recommended to use `LlamaModel::token_to_str` instead, as 8 bytes is typically sufficient for most tokens,
1232    /// and the extra buffer size doesn't usually matter.
1233    ///
1234    /// # Errors
1235    ///
1236    /// - If the token type is unknown, an error will be returned.
1237    /// - If the resultant token exceeds the provided `buffer_size`, an error will occur.
1238    /// - If the token string returned by `llama-cpp` is not valid UTF-8, it will return an error.
1239    ///
1240    /// # Panics
1241    ///
1242    /// - This function will panic if the `buffer_size` does not fit into a `c_int`.
1243    /// - It will also panic if the size returned from `llama-cpp` does not fit into a `usize`, which should typically never happen.
1244    ///
1245    /// # Example
1246    ///
1247    /// ```no_run
1248    /// use llama_cpp_4::model::{LlamaModel, Special};
1249    /// use llama_cpp_4::model::params::LlamaModelParams;
1250    /// use llama_cpp_4::llama_backend::LlamaBackend;
1251    /// use llama_cpp_4::token::LlamaToken;
1252    ///
1253    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1254    /// let backend = LlamaBackend::init()?;
1255    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1256    /// let token = LlamaToken::new(42);
1257    /// let token_string = model.token_to_str_with_size(token, 32, Special::Plaintext)?;
1258    /// # Ok(())
1259    /// # }
1260    /// ```
1261    pub fn token_to_str_with_size(
1262        &self,
1263        token: LlamaToken,
1264        buffer_size: usize,
1265        special: Special,
1266    ) -> Result<String, TokenToStringError> {
1267        let bytes = self.token_to_bytes_with_size(token, buffer_size, special, None)?;
1268        Ok(String::from_utf8(bytes)?)
1269    }
1270
1271    /// Convert a token to bytes with a specified buffer size.
1272    ///
1273    /// Generally you should use [`LlamaModel::token_to_bytes`] instead as 8 bytes is enough for most words and
1274    /// the extra bytes do not really matter.
1275    ///
1276    /// # Errors
1277    ///
1278    /// - if the token type is unknown
1279    /// - the resultant token is larger than `buffer_size`.
1280    ///
1281    /// # Panics
1282    ///
1283    /// - This function will panic if `buffer_size` cannot fit into a `c_int`.
1284    /// - It will also panic if the size returned from `llama-cpp` cannot be converted to `usize` (which should not happen).
1285    ///
1286    /// # Example
1287    ///
1288    /// ```no_run
1289    /// use llama_cpp_4::model::{LlamaModel, Special};
1290    /// use llama_cpp_4::model::params::LlamaModelParams;
1291    /// use llama_cpp_4::llama_backend::LlamaBackend;
1292    /// use llama_cpp_4::token::LlamaToken;
1293    ///
1294    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1295    /// let backend = LlamaBackend::init()?;
1296    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1297    /// let token = LlamaToken::new(42);
1298    /// let token_bytes = model.token_to_bytes_with_size(token, 32, Special::Plaintext, None)?;
1299    /// # Ok(())
1300    /// # }
1301    /// ```
1302    pub fn token_to_bytes_with_size(
1303        &self,
1304        token: LlamaToken,
1305        buffer_size: usize,
1306        special: Special,
1307        lstrip: Option<NonZeroU16>,
1308    ) -> Result<Vec<u8>, TokenToStringError> {
1309        if token == self.token_nl() {
1310            return Ok(String::from("\n").into_bytes());
1311        }
1312
1313        // unsure what to do with this in the face of the 'special' arg + attr changes
1314        let attrs = self.token_attr(token);
1315        if (attrs.contains(LlamaTokenAttr::Control)
1316            && (token == self.token_bos() || token == self.token_eos()))
1317            || attrs.is_empty()
1318            || attrs
1319                .intersects(LlamaTokenAttr::Unknown | LlamaTokenAttr::Byte | LlamaTokenAttr::Unused)
1320        {
1321            return Ok(Vec::new());
1322        }
1323
1324        let special = match special {
1325            Special::Tokenize => true,
1326            Special::Plaintext => false,
1327        };
1328
1329        let string = CString::new(vec![b'*'; buffer_size]).expect("no null");
1330        let len = string.as_bytes().len();
1331        let len = c_int::try_from(len).expect("length fits into c_int");
1332        let buf = string.into_raw();
1333        let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
1334        let size = unsafe {
1335            llama_token_to_piece(
1336                self.get_vocab().vocab.as_ref(),
1337                token.0,
1338                buf,
1339                len,
1340                lstrip,
1341                special,
1342            )
1343        };
1344
1345        match size {
1346            0 => Err(TokenToStringError::UnknownTokenType),
1347            i if i.is_negative() => Err(TokenToStringError::InsufficientBufferSpace(i)),
1348            size => {
1349                let string = unsafe { CString::from_raw(buf) };
1350                let mut bytes = string.into_bytes();
1351                let len = usize::try_from(size).expect("size is positive and fits into usize");
1352                bytes.truncate(len);
1353                Ok(bytes)
1354            }
1355        }
1356    }
1357
1358    /// Convert a token to raw llama.cpp piece bytes with a specified buffer size.
1359    ///
1360    /// This intentionally bypasses the token-attribute filtering in
1361    /// [`LlamaModel::token_to_bytes_with_size`] and forwards directly to
1362    /// `llama_token_to_piece`.
1363    ///
1364    /// # Errors
1365    ///
1366    /// - if llama.cpp reports an unknown token type.
1367    /// - if the resultant token is larger than `buffer_size`.
1368    ///
1369    /// # Panics
1370    ///
1371    /// This function will panic if `buffer_size` cannot fit into a `c_int`.
1372    pub fn token_to_raw_bytes_with_size(
1373        &self,
1374        token: LlamaToken,
1375        buffer_size: usize,
1376        special: Special,
1377        lstrip: Option<NonZeroU16>,
1378    ) -> Result<Vec<u8>, TokenToStringError> {
1379        let special = match special {
1380            Special::Tokenize => true,
1381            Special::Plaintext => false,
1382        };
1383        let mut buffer = vec![0_u8; buffer_size];
1384        let len = c_int::try_from(buffer.len()).expect("length fits into c_int");
1385        let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
1386        let size = unsafe {
1387            llama_token_to_piece(
1388                self.get_vocab().vocab.as_ref(),
1389                token.0,
1390                buffer.as_mut_ptr().cast::<c_char>(),
1391                len,
1392                lstrip,
1393                special,
1394            )
1395        };
1396
1397        match size {
1398            0 => Err(TokenToStringError::UnknownTokenType),
1399            i if i.is_negative() => Err(TokenToStringError::InsufficientBufferSpace(i)),
1400            size => {
1401                let len = usize::try_from(size).expect("size is positive and fits into usize");
1402                buffer.truncate(len);
1403                Ok(buffer)
1404            }
1405        }
1406    }
1407    /// The number of tokens the model was trained on.
1408    ///
1409    /// This function returns the number of tokens the model was trained on. It is returned as a `c_int` for maximum
1410    /// compatibility with the underlying llama-cpp library, though it can typically be cast to an `i32` without issue.
1411    ///
1412    /// # Example
1413    ///
1414    /// ```no_run
1415    /// use llama_cpp_4::model::LlamaModel;
1416    /// use llama_cpp_4::model::params::LlamaModelParams;
1417    /// use llama_cpp_4::llama_backend::LlamaBackend;
1418    ///
1419    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1420    /// let backend = LlamaBackend::init()?;
1421    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1422    /// let n_vocab = model.n_vocab();
1423    /// # Ok(())
1424    /// # }
1425    /// ```
1426    #[must_use]
1427    pub fn n_vocab(&self) -> i32 {
1428        self.get_vocab().n_tokens()
1429    }
1430
1431    /// The type of vocab the model was trained on.
1432    ///
1433    /// This function returns the type of vocabulary used by the model, such as whether it is based on byte-pair encoding (BPE),
1434    /// word-level tokens, or another tokenization scheme.
1435    ///
1436    /// # Panics
1437    ///
1438    /// - This function will panic if `llama-cpp` emits a vocab type that is not recognized or is invalid for this library.
1439    ///
1440    /// # Example
1441    ///
1442    /// ```no_run
1443    /// use llama_cpp_4::model::LlamaModel;
1444    /// use llama_cpp_4::model::params::LlamaModelParams;
1445    /// use llama_cpp_4::llama_backend::LlamaBackend;
1446    ///
1447    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1448    /// let backend = LlamaBackend::init()?;
1449    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1450    /// let vocab_type = model.vocab_type();
1451    /// # Ok(())
1452    /// # }
1453    /// ```
1454    #[must_use]
1455    pub fn vocab_type(&self) -> VocabType {
1456        let vocab_type = unsafe { llama_vocab_type(self.get_vocab().vocab.as_ref()) };
1457        VocabType::try_from(vocab_type).expect("invalid vocab type")
1458    }
1459
1460    /// Returns the number of embedding dimensions for the model.
1461    ///
1462    /// This function retrieves the number of embeddings (or embedding dimensions) used by the model. It is typically
1463    /// used for analyzing model architecture and setting up context parameters or other model configuration aspects.
1464    ///
1465    /// # Panics
1466    ///
1467    /// - This function may panic if the underlying `llama-cpp` library returns an invalid embedding dimension value.
1468    ///
1469    /// # Example
1470    ///
1471    /// ```no_run
1472    /// use llama_cpp_4::model::LlamaModel;
1473    /// use llama_cpp_4::model::params::LlamaModelParams;
1474    /// use llama_cpp_4::llama_backend::LlamaBackend;
1475    ///
1476    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1477    /// let backend = LlamaBackend::init()?;
1478    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1479    /// let n_embd = model.n_embd();
1480    /// # Ok(())
1481    /// # }
1482    /// ```
1483    #[must_use]
1484    pub fn n_embd(&self) -> c_int {
1485        unsafe { llama_model_n_embd(self.model.as_ptr()) }
1486    }
1487
1488    /// Get the number of transformer layers in the model.
1489    #[must_use]
1490    pub fn n_layer(&self) -> c_int {
1491        unsafe { llama_model_n_layer(self.model.as_ptr()) }
1492    }
1493
1494    /// Get the number of `NextN` / MTP prediction heads bundled with the model.
1495    ///
1496    /// Returns `0` when the checkpoint has no `NextN` blocks. Multi-head models
1497    /// (e.g. Step3.5) return values greater than `1`; pair with
1498    /// [`crate::context::LlamaContext::set_nextn_layer_offset`] on the draft
1499    /// context. See [`crate::mtp`] for the speculative-decoding workflow.
1500    ///
1501    /// # Examples
1502    ///
1503    /// ```no_run
1504    /// # use llama_cpp_4::llama_backend::LlamaBackend;
1505    /// # use llama_cpp_4::model::{LlamaModel, params::LlamaModelParams};
1506    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1507    /// # let backend = LlamaBackend::init()?;
1508    /// # let model = LlamaModel::load_from_file(&backend, "model.gguf", &LlamaModelParams::default())?;
1509    /// if model.n_layer_nextn() > 0 {
1510    ///     println!("MTP model with {} NextN heads", model.n_layer_nextn());
1511    /// }
1512    /// # Ok(())
1513    /// # }
1514    /// ```
1515    #[must_use]
1516    pub fn n_layer_nextn(&self) -> c_int {
1517        unsafe { llama_model_n_layer_nextn(self.model.as_ptr()) }
1518    }
1519
1520    /// Get the number of mixture-of-experts (`MoE`) layers in the model.
1521    ///
1522    /// Returns `0` for dense (non-MoE) checkpoints.
1523    #[must_use]
1524    pub fn n_expert(&self) -> c_int {
1525        unsafe { llama_model_n_expert(self.model.as_ptr()) }
1526    }
1527
1528    /// Number of backend devices the model tensors are spread across.
1529    ///
1530    /// Use with [`Self::get_device`] to inspect each device. Returns `0` when
1531    /// the model is not yet loaded onto any device.
1532    #[must_use]
1533    pub fn n_devices(&self) -> c_int {
1534        unsafe { llama_model_n_devices(self.model.as_ptr()) }
1535    }
1536
1537    /// Get the backend device at `index`.
1538    ///
1539    /// Valid indices satisfy `0 <= index < n_devices()`. Returns `None` for
1540    /// out-of-range indices or when the device pointer is null.
1541    #[must_use]
1542    pub fn get_device(&self, index: i32) -> Option<LlamaBackendDevice> {
1543        if index < 0 || index >= self.n_devices() {
1544            return None;
1545        }
1546        let dev = unsafe { llama_model_get_device(self.model.as_ptr(), index) };
1547        if dev.is_null() {
1548            None
1549        } else {
1550            Some(LlamaBackendDevice { dev })
1551        }
1552    }
1553
1554    /// Iterate backend devices the model tensors are spread across.
1555    ///
1556    /// Equivalent to calling [`Self::get_device`] for `0..self.n_devices()`.
1557    /// Use [`LlamaBackendDevice::memory`] to inspect free/total bytes per device.
1558    #[must_use]
1559    pub fn devices(&self) -> LlamaBackendDevices<'_> {
1560        LlamaBackendDevices {
1561            model: self,
1562            next: 0,
1563        }
1564    }
1565
1566    /// Target-model layer indices stored in this checkpoint.
1567    ///
1568    /// Populated for EAGLE / distillation draft models that record which target
1569    /// layers they were trained against. Returns an empty slice when the
1570    /// metadata is absent.
1571    #[must_use]
1572    pub fn target_layer_ids(&self) -> &[i32] {
1573        let n = unsafe { llama_model_target_layer_ids_n(self.model.as_ptr()) };
1574        if n == 0 {
1575            return &[];
1576        }
1577        let ptr = unsafe { llama_model_target_layer_ids(self.model.as_ptr()) };
1578        if ptr.is_null() {
1579            &[]
1580        } else {
1581            unsafe { slice::from_raw_parts(ptr, n as usize) }
1582        }
1583    }
1584
1585    /// Get the number of attention heads in the model.
1586    #[must_use]
1587    pub fn n_head(&self) -> c_int {
1588        unsafe { llama_model_n_head(self.model.as_ptr()) }
1589    }
1590
1591    /// Get the number of key-value attention heads in the model.
1592    #[must_use]
1593    pub fn n_head_kv(&self) -> c_int {
1594        unsafe { llama_model_n_head_kv(self.model.as_ptr()) }
1595    }
1596
1597    /// Get the input embedding size of the model.
1598    #[must_use]
1599    pub fn n_embd_inp(&self) -> c_int {
1600        unsafe { llama_model_n_embd_inp(self.model.as_ptr()) }
1601    }
1602
1603    /// Get the output embedding size of the model.
1604    #[must_use]
1605    pub fn n_embd_out(&self) -> c_int {
1606        unsafe { llama_model_n_embd_out(self.model.as_ptr()) }
1607    }
1608
1609    /// Get the sliding window attention size of the model.
1610    /// Returns 0 if the model does not use sliding window attention.
1611    #[must_use]
1612    pub fn n_swa(&self) -> c_int {
1613        unsafe { llama_model_n_swa(self.model.as_ptr()) }
1614    }
1615
1616    /// Get the `RoPE` type used by the model.
1617    #[must_use]
1618    pub fn rope_type(&self) -> i32 {
1619        unsafe { llama_model_rope_type(self.model.as_ptr()) }
1620    }
1621
1622    /// Get the `RoPE` frequency scale used during training.
1623    #[must_use]
1624    pub fn rope_freq_scale_train(&self) -> f32 {
1625        unsafe { llama_model_rope_freq_scale_train(self.model.as_ptr()) }
1626    }
1627
1628    /// Get the model size in bytes.
1629    #[must_use]
1630    pub fn model_size(&self) -> u64 {
1631        unsafe { llama_model_size(self.model.as_ptr()) }
1632    }
1633
1634    /// Get the number of parameters in the model.
1635    #[must_use]
1636    pub fn n_params(&self) -> u64 {
1637        unsafe { llama_model_n_params(self.model.as_ptr()) }
1638    }
1639
1640    /// Get the number of classification outputs.
1641    #[must_use]
1642    pub fn n_cls_out(&self) -> u32 {
1643        unsafe { llama_model_n_cls_out(self.model.as_ptr()) }
1644    }
1645
1646    /// Get the classification label for the given index.
1647    ///
1648    /// # Errors
1649    ///
1650    /// Returns an error if the label is null or not valid UTF-8.
1651    pub fn cls_label(&self, index: u32) -> Result<&str, StringFromModelError> {
1652        let ptr = unsafe { llama_model_cls_label(self.model.as_ptr(), index) };
1653        if ptr.is_null() {
1654            return Err(StringFromModelError::ReturnedError(-1));
1655        }
1656        let cstr = unsafe { CStr::from_ptr(ptr) };
1657        cstr.to_str().map_err(StringFromModelError::Utf8Error)
1658    }
1659
1660    /// Adopt a raw `llama_model *`, taking ownership.
1661    ///
1662    /// # Safety
1663    ///
1664    /// `raw` must come from a llama.cpp entry point documented as returning a
1665    /// model the caller must release with `llama_model_free`, and must not be
1666    /// owned by anything else — [`Drop`] frees it.
1667    pub(crate) unsafe fn from_raw(raw: NonNull<llama_model>) -> Self {
1668        Self { model: raw }
1669    }
1670
1671    /// Get the number of metadata key-value pairs.
1672    #[must_use]
1673    pub fn meta_count(&self) -> c_int {
1674        unsafe { llama_model_meta_count(self.model.as_ptr()) }
1675    }
1676
1677    /// Get a chat template baked into the model, by name.
1678    ///
1679    /// Pass `None` for the default template (GGUF key `tokenizer.chat_template`),
1680    /// or a name to reach a variant — `Some("tool_use")` resolves
1681    /// `tokenizer.chat_template.tool_use`, which several models ship alongside
1682    /// their default and which is the one you want when the request carries
1683    /// tools.
1684    ///
1685    /// Supersedes the deprecated `get_chat_template`, which reads the default
1686    /// key straight out of GGUF metadata into a buffer the caller has to size —
1687    /// too small and it fails, too large and it wastes the allocation. This
1688    /// borrows a pointer llama.cpp already owns, so neither applies, and it can
1689    /// reach the named variants the other cannot.
1690    ///
1691    /// # Errors
1692    ///
1693    /// Returns [`StringFromModelError::ReturnedError`] if the model has no such
1694    /// template, or [`StringFromModelError::Utf8Error`] if it is not UTF-8.
1695    pub fn chat_template(&self, name: Option<&str>) -> Result<&str, StringFromModelError> {
1696        let c_name = match name {
1697            Some(n) => Some(CString::new(n).map_err(|_| StringFromModelError::ReturnedError(-1))?),
1698            None => None,
1699        };
1700        let name_ptr = c_name.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
1701        let ptr = unsafe { llama_model_chat_template(self.model.as_ptr(), name_ptr) };
1702        if ptr.is_null() {
1703            return Err(StringFromModelError::ReturnedError(-1));
1704        }
1705        let cstr = unsafe { CStr::from_ptr(ptr) };
1706        cstr.to_str().map_err(StringFromModelError::Utf8Error)
1707    }
1708
1709    /// Get the model's file type — the quantization it was stored with.
1710    ///
1711    /// Returns `None` for a type this crate's [`LlamaFtype`] does not know,
1712    /// which is what you get for `LLAMA_FTYPE_GUESSED` (the file did not
1713    /// specify one) or a type added upstream since this release.
1714    #[must_use]
1715    pub fn ftype(&self) -> Option<LlamaFtype> {
1716        let raw = unsafe { llama_model_ftype(self.model.as_ptr()) };
1717        LlamaFtype::try_from(raw).ok()
1718    }
1719
1720    /// Copy the whole token-embedding matrix out as `f32`, row-major and
1721    /// `n_vocab * n_embd` long.
1722    ///
1723    /// llama.cpp converts from whatever the tensor is stored as, so this
1724    /// allocates `4 * n_vocab * n_embd` bytes — 500 MB for a 128k-vocab 1024-dim
1725    /// model. Wraps `llama_model_get_tok_embd`.
1726    ///
1727    /// # Errors
1728    ///
1729    /// Returns [`StringFromModelError::ReturnedError`] if the model exposes no
1730    /// token-embedding tensor.
1731    pub fn token_embeddings(&self) -> Result<Vec<f32>, StringFromModelError> {
1732        // A null `out` asks for the element count without writing anything.
1733        let n = unsafe { llama_model_get_tok_embd(self.model.as_ptr(), std::ptr::null_mut()) };
1734        if n == 0 {
1735            return Err(StringFromModelError::ReturnedError(-1));
1736        }
1737        let mut out = vec![0.0_f32; n as usize];
1738        let written = unsafe { llama_model_get_tok_embd(self.model.as_ptr(), out.as_mut_ptr()) };
1739        if written == 0 {
1740            return Err(StringFromModelError::ReturnedError(-1));
1741        }
1742        // Trust the second call's count over the first: a shrink would otherwise
1743        // leave uninitialised-looking zeros on the tail.
1744        out.truncate(written as usize);
1745        Ok(out)
1746    }
1747
1748    /// Get a model description string.
1749    ///
1750    /// The `buf_size` parameter specifies the maximum buffer size for the description.
1751    /// A default of 256 bytes is usually sufficient.
1752    ///
1753    /// # Errors
1754    ///
1755    /// Returns an error if the description could not be retrieved or is not valid UTF-8.
1756    #[allow(clippy::cast_sign_loss)]
1757    pub fn desc(&self, buf_size: usize) -> Result<String, StringFromModelError> {
1758        let mut buf = vec![0u8; buf_size];
1759        let ret = unsafe {
1760            llama_model_desc(
1761                self.model.as_ptr(),
1762                buf.as_mut_ptr().cast::<c_char>(),
1763                buf_size,
1764            )
1765        };
1766        if ret < 0 {
1767            return Err(StringFromModelError::ReturnedError(ret));
1768        }
1769        let len = ret as usize;
1770        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1771        Ok(s.to_owned())
1772    }
1773
1774    /// Get a metadata key by index.
1775    ///
1776    /// The `buf_size` parameter specifies the maximum buffer size for the key.
1777    /// A default of 256 bytes is usually sufficient.
1778    ///
1779    /// # Errors
1780    ///
1781    /// Returns an error if the index is out of range or the key is not valid UTF-8.
1782    #[allow(clippy::cast_sign_loss)]
1783    pub fn meta_key_by_index(
1784        &self,
1785        index: i32,
1786        buf_size: usize,
1787    ) -> Result<String, StringFromModelError> {
1788        let mut buf = vec![0u8; buf_size];
1789        let ret = unsafe {
1790            llama_model_meta_key_by_index(
1791                self.model.as_ptr(),
1792                index,
1793                buf.as_mut_ptr().cast::<c_char>(),
1794                buf_size,
1795            )
1796        };
1797        if ret < 0 {
1798            return Err(StringFromModelError::ReturnedError(ret));
1799        }
1800        let len = ret as usize;
1801        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1802        Ok(s.to_owned())
1803    }
1804
1805    /// Get a metadata value string by index.
1806    ///
1807    /// The `buf_size` parameter specifies the maximum buffer size for the value.
1808    /// Values can be large (e.g. chat templates, token lists), so 4096+ may be needed.
1809    ///
1810    /// # Errors
1811    ///
1812    /// Returns an error if the index is out of range or the value is not valid UTF-8.
1813    #[allow(clippy::cast_sign_loss)]
1814    pub fn meta_val_str_by_index(
1815        &self,
1816        index: i32,
1817        buf_size: usize,
1818    ) -> Result<String, StringFromModelError> {
1819        let mut buf = vec![0u8; buf_size];
1820        let ret = unsafe {
1821            llama_model_meta_val_str_by_index(
1822                self.model.as_ptr(),
1823                index,
1824                buf.as_mut_ptr().cast::<c_char>(),
1825                buf_size,
1826            )
1827        };
1828        if ret < 0 {
1829            return Err(StringFromModelError::ReturnedError(ret));
1830        }
1831        let len = ret as usize;
1832        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1833        Ok(s.to_owned())
1834    }
1835
1836    /// Get a metadata value by key name.
1837    ///
1838    /// This is more convenient than iterating metadata by index when you know the key.
1839    /// The `buf_size` parameter specifies the maximum buffer size for the value.
1840    ///
1841    /// # Errors
1842    ///
1843    /// Returns an error if the key is not found, contains a null byte, or the value is not valid UTF-8.
1844    #[allow(clippy::cast_sign_loss)]
1845    pub fn meta_val_str(&self, key: &str, buf_size: usize) -> Result<String, StringFromModelError> {
1846        let c_key = CString::new(key).map_err(|_| StringFromModelError::ReturnedError(-1))?;
1847        let mut buf = vec![0u8; buf_size];
1848        let ret = unsafe {
1849            llama_model_meta_val_str(
1850                self.model.as_ptr(),
1851                c_key.as_ptr(),
1852                buf.as_mut_ptr().cast::<c_char>(),
1853                buf_size,
1854            )
1855        };
1856        if ret < 0 {
1857            return Err(StringFromModelError::ReturnedError(ret));
1858        }
1859        let len = ret as usize;
1860        let s = std::str::from_utf8(&buf[..len]).map_err(StringFromModelError::Utf8Error)?;
1861        Ok(s.to_owned())
1862    }
1863
1864    /// Get all metadata as a list of `(key, value)` pairs.
1865    ///
1866    /// This is a convenience method that iterates over all metadata entries.
1867    /// Keys use a buffer of 256 bytes and values use 4096 bytes.
1868    /// For values that may be larger (e.g. token lists), use
1869    /// [`meta_val_str_by_index`](Self::meta_val_str_by_index) directly with a larger buffer.
1870    ///
1871    /// # Errors
1872    ///
1873    /// Returns an error if any key or value cannot be read or is not valid UTF-8.
1874    #[allow(clippy::cast_sign_loss)]
1875    pub fn metadata(&self) -> Result<Vec<(String, String)>, StringFromModelError> {
1876        let count = self.meta_count();
1877        let mut result = Vec::with_capacity(count as usize);
1878        for i in 0..count {
1879            let key = self.meta_key_by_index(i, 256)?;
1880            let val = self.meta_val_str_by_index(i, 4096)?;
1881            result.push((key, val));
1882        }
1883        Ok(result)
1884    }
1885
1886    /// Check if the model has an encoder.
1887    #[must_use]
1888    pub fn has_encoder(&self) -> bool {
1889        unsafe { llama_model_has_encoder(self.model.as_ptr()) }
1890    }
1891
1892    /// Check if the model has a decoder.
1893    #[must_use]
1894    pub fn has_decoder(&self) -> bool {
1895        unsafe { llama_model_has_decoder(self.model.as_ptr()) }
1896    }
1897
1898    /// Check if the model is recurrent (e.g. Mamba, RWKV).
1899    #[must_use]
1900    pub fn is_recurrent(&self) -> bool {
1901        unsafe { llama_model_is_recurrent(self.model.as_ptr()) }
1902    }
1903
1904    /// Check if the model is a hybrid model.
1905    #[must_use]
1906    pub fn is_hybrid(&self) -> bool {
1907        unsafe { llama_model_is_hybrid(self.model.as_ptr()) }
1908    }
1909
1910    /// Check if the model is a diffusion model.
1911    #[must_use]
1912    pub fn is_diffusion(&self) -> bool {
1913        unsafe { llama_model_is_diffusion(self.model.as_ptr()) }
1914    }
1915
1916    /// Get chat template from model.
1917    ///
1918    /// # Errors
1919    ///
1920    /// - If the model does not have a chat template, it will return an error.
1921    /// - If the chat template is not a valid `CString`, it will return an error.
1922    ///
1923    /// # Example
1924    ///
1925    /// ```no_run
1926    /// use llama_cpp_4::model::LlamaModel;
1927    /// use llama_cpp_4::model::params::LlamaModelParams;
1928    /// use llama_cpp_4::llama_backend::LlamaBackend;
1929    ///
1930    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1931    /// let backend = LlamaBackend::init()?;
1932    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1933    /// let chat_template = model.get_chat_template(1024)?;
1934    /// # Ok(())
1935    /// # }
1936    /// ```
1937    #[deprecated(
1938        since = "0.7.0",
1939        note = "use `chat_template(None)`, which borrows llama.cpp's own pointer \
1940                instead of copying into a caller-sized buffer, and can reach named \
1941                variants such as `chat_template(Some(\"tool_use\"))`"
1942    )]
1943    #[allow(clippy::missing_panics_doc)] // We statically know this will not panic as long as the buffer size is sufficient
1944    pub fn get_chat_template(&self, buf_size: usize) -> Result<String, ChatTemplateError> {
1945        // longest known template is about 1200 bytes from llama.cpp
1946        let chat_temp = CString::new(vec![b'*'; buf_size]).expect("no null");
1947        let chat_ptr = chat_temp.into_raw();
1948        let chat_name = CString::new("tokenizer.chat_template").expect("no null bytes");
1949
1950        let ret = unsafe {
1951            llama_model_meta_val_str(self.model.as_ptr(), chat_name.as_ptr(), chat_ptr, buf_size)
1952        };
1953
1954        if ret < 0 {
1955            return Err(ChatTemplateError::MissingTemplate(ret));
1956        }
1957
1958        let template_c = unsafe { CString::from_raw(chat_ptr) };
1959        let template = template_c.to_str()?;
1960
1961        let ret: usize = ret.try_into().unwrap();
1962        if template.len() < ret {
1963            return Err(ChatTemplateError::BuffSizeError(ret + 1));
1964        }
1965
1966        Ok(template.to_owned())
1967    }
1968
1969    /// Loads a model from a file.
1970    ///
1971    /// This function loads a model from a specified file path and returns the corresponding `LlamaModel` instance.
1972    ///
1973    /// # Errors
1974    ///
1975    /// - If the path cannot be converted to a string or if the model file does not exist, it will return an error.
1976    /// - If the model cannot be loaded (e.g., due to an invalid or corrupted model file), it will return a `LlamaModelLoadError`.
1977    ///
1978    /// # Example
1979    ///
1980    /// ```no_run
1981    /// use llama_cpp_4::model::LlamaModel;
1982    /// use llama_cpp_4::model::params::LlamaModelParams;
1983    /// use llama_cpp_4::llama_backend::LlamaBackend;
1984    ///
1985    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1986    /// let backend = LlamaBackend::init()?;
1987    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
1988    /// # Ok(())
1989    /// # }
1990    /// ```
1991    #[tracing::instrument(skip_all, fields(params))]
1992    pub fn load_from_file(
1993        _: &LlamaBackend,
1994        path: impl AsRef<Path>,
1995        params: &LlamaModelParams,
1996    ) -> Result<Self, LlamaModelLoadError> {
1997        let path = path.as_ref();
1998        debug_assert!(
1999            Path::new(path).exists(),
2000            "{} does not exist",
2001            path.display()
2002        );
2003        let path = path
2004            .to_str()
2005            .ok_or(LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
2006
2007        let cstr = CString::new(path)?;
2008        let llama_model = unsafe { llama_model_load_from_file(cstr.as_ptr(), params.params) };
2009
2010        let model = NonNull::new(llama_model).ok_or(LlamaModelLoadError::NullResult)?;
2011
2012        tracing::debug!(?path, "Loaded model");
2013        Ok(LlamaModel { model })
2014    }
2015
2016    /// Load a model from multiple split files.
2017    ///
2018    /// This function loads a model that has been split across multiple files. This is useful for
2019    /// very large models that exceed filesystem limitations or need to be distributed across
2020    /// multiple storage devices.
2021    ///
2022    /// # Arguments
2023    ///
2024    /// * `paths` - A slice of paths to the split model files
2025    /// * `params` - The model parameters
2026    ///
2027    /// # Errors
2028    ///
2029    /// Returns an error if:
2030    /// - Any of the paths cannot be converted to a C string
2031    /// - The model fails to load from the splits
2032    /// - Any path doesn't exist or isn't accessible
2033    ///
2034    /// # Example
2035    ///
2036    /// ```no_run
2037    /// use llama_cpp_4::model::{LlamaModel, params::LlamaModelParams};
2038    /// use llama_cpp_4::llama_backend::LlamaBackend;
2039    ///
2040    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2041    /// let backend = LlamaBackend::init()?;
2042    /// let params = LlamaModelParams::default();
2043    ///
2044    /// let paths = vec![
2045    ///     "model-00001-of-00003.gguf",
2046    ///     "model-00002-of-00003.gguf",
2047    ///     "model-00003-of-00003.gguf",
2048    /// ];
2049    ///
2050    /// let model = LlamaModel::load_from_splits(&backend, &paths, &params)?;
2051    /// # Ok(())
2052    /// # }
2053    /// ```
2054    #[tracing::instrument(skip_all)]
2055    pub fn load_from_splits(
2056        _: &LlamaBackend,
2057        paths: &[impl AsRef<Path>],
2058        params: &LlamaModelParams,
2059    ) -> Result<Self, LlamaModelLoadError> {
2060        // Convert paths to C strings
2061        let c_strings: Vec<CString> = paths
2062            .iter()
2063            .map(|p| {
2064                let path = p.as_ref();
2065                debug_assert!(path.exists(), "{} does not exist", path.display());
2066                let path_str = path
2067                    .to_str()
2068                    .ok_or(LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
2069                CString::new(path_str).map_err(LlamaModelLoadError::from)
2070            })
2071            .collect::<Result<Vec<_>, _>>()?;
2072
2073        // Create array of pointers to C strings
2074        let c_ptrs: Vec<*const c_char> = c_strings.iter().map(|s| s.as_ptr()).collect();
2075
2076        // Load the model from splits
2077        let llama_model = unsafe {
2078            llama_model_load_from_splits(c_ptrs.as_ptr().cast_mut(), c_ptrs.len(), params.params)
2079        };
2080
2081        let model = NonNull::new(llama_model).ok_or(LlamaModelLoadError::NullResult)?;
2082
2083        tracing::debug!("Loaded model from {} splits", paths.len());
2084        Ok(LlamaModel { model })
2085    }
2086
2087    /// Load a model from a `FILE` pointer.
2088    ///
2089    /// # Safety
2090    ///
2091    /// The `file` pointer must be a valid, open `FILE*`.
2092    ///
2093    /// # Errors
2094    ///
2095    /// Returns an error if the model cannot be loaded.
2096    pub unsafe fn load_from_file_ptr(
2097        file: *mut llama_cpp_sys_4::FILE,
2098        params: &LlamaModelParams,
2099    ) -> Result<Self, LlamaModelLoadError> {
2100        let model = llama_cpp_sys_4::llama_model_load_from_file_ptr(file, params.params);
2101        let model = NonNull::new(model).ok_or(LlamaModelLoadError::NullResult)?;
2102        Ok(LlamaModel { model })
2103    }
2104
2105    /// Initialize a model from user-provided data.
2106    ///
2107    /// # Safety
2108    ///
2109    /// The metadata, callback, and user data must be valid.
2110    ///
2111    /// # Errors
2112    ///
2113    /// Returns an error if the model cannot be initialized.
2114    pub unsafe fn init_from_user(
2115        metadata: *mut llama_cpp_sys_4::gguf_context,
2116        set_tensor_data: llama_cpp_sys_4::llama_model_set_tensor_data_t,
2117        set_tensor_data_ud: *mut std::ffi::c_void,
2118        params: &LlamaModelParams,
2119    ) -> Result<Self, LlamaModelLoadError> {
2120        let model = llama_cpp_sys_4::llama_model_init_from_user(
2121            metadata,
2122            set_tensor_data,
2123            set_tensor_data_ud,
2124            params.params,
2125        );
2126        let model = NonNull::new(model).ok_or(LlamaModelLoadError::NullResult)?;
2127        Ok(LlamaModel { model })
2128    }
2129
2130    /// Save the model to a file.
2131    ///
2132    /// # Panics
2133    ///
2134    /// Panics if the path contains null bytes.
2135    pub fn save_to_file(&self, path: impl AsRef<Path>) {
2136        let path = path.as_ref();
2137        let path_str = path.to_str().expect("path is not valid UTF-8");
2138        let c_path = CString::new(path_str).expect("path contains null bytes");
2139        unsafe {
2140            llama_model_save_to_file(self.model.as_ptr(), c_path.as_ptr());
2141        }
2142    }
2143
2144    /// Get the list of built-in chat templates.
2145    ///
2146    /// Returns the names of all chat templates that are built into llama.cpp.
2147    ///
2148    /// # Panics
2149    ///
2150    /// Panics if any template name is not valid UTF-8.
2151    #[allow(clippy::cast_sign_loss)]
2152    #[must_use]
2153    pub fn chat_builtin_templates() -> Vec<String> {
2154        // First call to get count
2155        let count = unsafe { llama_chat_builtin_templates(std::ptr::null_mut(), 0) };
2156        if count <= 0 {
2157            return Vec::new();
2158        }
2159        let count = count as usize;
2160        let mut ptrs: Vec<*const c_char> = vec![std::ptr::null(); count];
2161        unsafe {
2162            llama_chat_builtin_templates(ptrs.as_mut_ptr(), count);
2163        }
2164        ptrs.iter()
2165            .map(|&p| {
2166                let cstr = unsafe { CStr::from_ptr(p) };
2167                cstr.to_str()
2168                    .expect("template name is not valid UTF-8")
2169                    .to_owned()
2170            })
2171            .collect()
2172    }
2173
2174    /// Initializes a lora adapter from a file.
2175    ///
2176    /// This function initializes a Lora adapter, which is a model extension used to adapt or fine-tune the existing model
2177    /// to a specific domain or task. The adapter file is typically in the form of a binary or serialized file that can be applied
2178    /// to the model for improved performance on specialized tasks.
2179    ///
2180    /// # Errors
2181    ///
2182    /// - If the adapter file path cannot be converted to a string or if the adapter cannot be initialized, it will return an error.
2183    ///
2184    /// # Example
2185    ///
2186    /// ```no_run
2187    /// use llama_cpp_4::model::LlamaModel;
2188    /// use llama_cpp_4::model::params::LlamaModelParams;
2189    /// use llama_cpp_4::llama_backend::LlamaBackend;
2190    ///
2191    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2192    /// let backend = LlamaBackend::init()?;
2193    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2194    /// let adapter = model.lora_adapter_init("path/to/lora/adapter")?;
2195    /// # Ok(())
2196    /// # }
2197    /// ```
2198    pub fn lora_adapter_init(
2199        &self,
2200        path: impl AsRef<Path>,
2201    ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
2202        let path = path.as_ref();
2203        debug_assert!(
2204            Path::new(path).exists(),
2205            "{} does not exist",
2206            path.display()
2207        );
2208
2209        let path = path
2210            .to_str()
2211            .ok_or(LlamaLoraAdapterInitError::PathToStrError(
2212                path.to_path_buf(),
2213            ))?;
2214
2215        let cstr = CString::new(path)?;
2216        let adapter = unsafe { llama_adapter_lora_init(self.model.as_ptr(), cstr.as_ptr()) };
2217
2218        let adapter = NonNull::new(adapter).ok_or(LlamaLoraAdapterInitError::NullResult)?;
2219
2220        tracing::debug!(?path, "Initialized lora adapter");
2221        Ok(LlamaLoraAdapter {
2222            lora_adapter: adapter,
2223        })
2224    }
2225
2226    /// Create a new context from this model.
2227    ///
2228    /// This function creates a new context for the model, which is used to manage and perform computations for inference,
2229    /// including token generation, embeddings, and other tasks that the model can perform. The context allows fine-grained
2230    /// control over model parameters for a specific task.
2231    ///
2232    /// # Errors
2233    ///
2234    /// - There are various potential failures such as invalid parameters or a failure to allocate the context. See [`LlamaContextLoadError`]
2235    ///   for more detailed error descriptions.
2236    ///
2237    /// # Example
2238    ///
2239    /// ```no_run
2240    /// use llama_cpp_4::model::LlamaModel;
2241    /// use llama_cpp_4::model::params::LlamaModelParams;
2242    /// use llama_cpp_4::context::params::LlamaContextParams;
2243    /// use llama_cpp_4::llama_backend::LlamaBackend;
2244    ///
2245    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2246    /// let backend = LlamaBackend::init()?;
2247    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2248    /// let context = model.new_context(&backend, LlamaContextParams::default())?;
2249    /// # Ok(())
2250    /// # }
2251    /// ```
2252    #[allow(clippy::needless_pass_by_value)]
2253    pub fn new_context(
2254        &self,
2255        _: &LlamaBackend,
2256        mut params: LlamaContextParams,
2257    ) -> Result<LlamaContext<'_>, LlamaContextLoadError> {
2258        // Apply TurboQuant attn-rotation preference before the KV cache is
2259        // initialised inside llama_init_from_model.
2260        let prev_rot_var = std::env::var("LLAMA_ATTN_ROT_DISABLE").ok();
2261        if params.attn_rot_disabled {
2262            // SAFETY: we restore the value right after the call.
2263            #[allow(unused_unsafe)]
2264            unsafe {
2265                std::env::set_var("LLAMA_ATTN_ROT_DISABLE", "1");
2266            }
2267        } else if std::env::var("LLAMA_ATTN_ROT_DISABLE").is_ok() {
2268            // params say "enabled" – only clear if it was previously unset
2269            // (respect explicit user env var).
2270        }
2271
2272        let context_type = params.ctx_type();
2273        let context_params = params.context_params;
2274        let embeddings_enabled = params.embeddings();
2275        let context = unsafe { llama_init_from_model(self.model.as_ptr(), context_params) };
2276
2277        // Restore the env-var to its previous state.
2278        #[allow(unused_unsafe)]
2279        match prev_rot_var {
2280            Some(v) => unsafe { std::env::set_var("LLAMA_ATTN_ROT_DISABLE", v) },
2281            None if params.attn_rot_disabled => unsafe {
2282                std::env::remove_var("LLAMA_ATTN_ROT_DISABLE");
2283            },
2284            None => {}
2285        }
2286
2287        let context = NonNull::new(context).ok_or(LlamaContextLoadError::NullReturn)?;
2288        let tensor_transactions = params.tensor_transactions.take();
2289        Ok(LlamaContext::new(
2290            self,
2291            context,
2292            embeddings_enabled,
2293            context_type,
2294            tensor_transactions,
2295        ))
2296    }
2297
2298    /// Apply the model's chat template to a sequence of messages.
2299    ///
2300    /// This function applies the model's chat template to the provided chat messages, formatting them accordingly. The chat
2301    /// template determines the structure or style of conversation between the system and user, such as token formatting,
2302    /// role separation, and more. The template can be customized by providing an optional template string, or if `None`
2303    /// is provided, the default template used by `llama.cpp` will be applied.
2304    ///
2305    /// For more information on supported templates, visit:
2306    /// <https://github.com/ggerganov/llama.cpp/wiki/Templates-supported-by-llama_chat_apply_template>
2307    ///
2308    /// # Arguments
2309    ///
2310    /// - `tmpl`: An optional custom template string. If `None`, the default template will be used.
2311    /// - `chat`: A vector of `LlamaChatMessage` instances, which represent the conversation between the system and user.
2312    /// - `add_ass`: A boolean flag indicating whether additional system-specific instructions (like "assistant") should be included.
2313    ///
2314    /// # Errors
2315    ///
2316    /// There are several possible points of failure when applying the chat template:
2317    /// - Insufficient buffer size to hold the formatted chat (this will return `ApplyChatTemplateError::BuffSizeError`).
2318    /// - If the template or messages cannot be processed properly, various errors from `ApplyChatTemplateError` may occur.
2319    ///
2320    /// # Example
2321    ///
2322    /// ```no_run
2323    /// use llama_cpp_4::model::{LlamaModel, LlamaChatMessage};
2324    /// use llama_cpp_4::model::params::LlamaModelParams;
2325    /// use llama_cpp_4::llama_backend::LlamaBackend;
2326    ///
2327    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2328    /// let backend = LlamaBackend::init()?;
2329    /// let model = LlamaModel::load_from_file(&backend, "path/to/model", &LlamaModelParams::default())?;
2330    /// let chat = vec![
2331    ///     LlamaChatMessage::new("user".to_string(), "Hello!".to_string())?,
2332    ///     LlamaChatMessage::new("assistant".to_string(), "Hi! How can I assist you today?".to_string())?,
2333    /// ];
2334    /// let formatted_chat = model.apply_chat_template(None, &chat, true)?;
2335    /// # Ok(())
2336    /// # }
2337    /// ```
2338    ///
2339    /// # Notes
2340    ///
2341    /// The provided buffer is twice the length of the messages by default, which is recommended by the `llama.cpp` documentation.
2342    /// # Panics
2343    ///
2344    /// Panics if the buffer length exceeds `i32::MAX`.
2345    #[tracing::instrument(skip_all)]
2346    pub fn apply_chat_template(
2347        &self,
2348        tmpl: Option<&str>,
2349        chat: &[LlamaChatMessage],
2350        add_ass: bool,
2351    ) -> Result<String, ApplyChatTemplateError> {
2352        // Compute raw message byte total from the original LlamaChatMessage vec
2353        // *before* we shadow `chat` with the sys-type vec below.
2354        let message_length = chat.iter().fold(0usize, |acc, c| {
2355            acc + c.role.to_bytes().len() + c.content.to_bytes().len()
2356        });
2357
2358        // Build our llama_cpp_sys chat messages (raw pointers into CStrings).
2359        let chat_sys: Vec<llama_chat_message> = chat
2360            .iter()
2361            .map(|c| llama_chat_message {
2362                role: c.role.as_ptr(),
2363                content: c.content.as_ptr(),
2364            })
2365            .collect();
2366
2367        // Set the tmpl pointer.
2368        let tmpl_cstring = tmpl.map(CString::new).transpose()?;
2369        let tmpl_ptr = tmpl_cstring
2370            .as_ref()
2371            .map_or(std::ptr::null(), |s| s.as_ptr());
2372
2373        // `message_length * 4` is far too small for models whose built-in chat
2374        // template adds a long default system prompt (e.g. Qwen3.5 prepends
2375        // ~80+ chars of markup even for a one-word user message).  Start with
2376        // at least 4 KiB so short inputs like "hi" always have room.
2377        //
2378        // `llama_chat_apply_template` returns the number of bytes it *actually*
2379        // needed when the buffer was too small, so we retry exactly once with
2380        // that precise size rather than giving up immediately.
2381        let mut buf_size = message_length.saturating_mul(4).max(4096);
2382
2383        for _ in 0..2 {
2384            // Use u8 so that as_mut_ptr()/as_ptr() match the binding (*mut u8 / *const u8).
2385            let mut buff = vec![0u8; buf_size];
2386            let res = unsafe {
2387                llama_chat_apply_template(
2388                    tmpl_ptr,
2389                    chat_sys.as_ptr(),
2390                    chat_sys.len(),
2391                    add_ass,
2392                    buff.as_mut_ptr().cast(),
2393                    i32::try_from(buff.len()).expect("buffer length fits in i32"),
2394                )
2395            };
2396
2397            if res < 0 {
2398                return Err(ApplyChatTemplateError::BuffSizeError);
2399            }
2400
2401            #[allow(clippy::cast_sign_loss)]
2402            let needed = res as usize;
2403            if needed > buf_size {
2404                // Buffer was too small — retry with the exact size llama.cpp reported.
2405                buf_size = needed + 1; // +1 for null terminator
2406                continue;
2407            }
2408
2409            // SAFETY: llama_chat_apply_template wrote a NUL-terminated string
2410            // into `buff`; `needed` bytes were used.
2411            let formatted = unsafe {
2412                CStr::from_ptr(buff.as_ptr().cast())
2413                    .to_string_lossy()
2414                    .into_owned()
2415            };
2416            return Ok(formatted);
2417        }
2418
2419        Err(ApplyChatTemplateError::BuffSizeError)
2420    }
2421
2422    /// Build a split GGUF file path for a specific chunk.
2423    ///
2424    /// This utility function creates the standardized filename for a split model chunk
2425    /// following the pattern: `{prefix}-{split_no:05d}-of-{split_count:05d}.gguf`
2426    ///
2427    /// # Arguments
2428    ///
2429    /// * `path_prefix` - The base path and filename prefix
2430    /// * `split_no` - The split number (1-indexed)
2431    /// * `split_count` - The total number of splits
2432    ///
2433    /// # Returns
2434    ///
2435    /// Returns the formatted split path as a String
2436    ///
2437    /// # Example
2438    ///
2439    /// ```
2440    /// use llama_cpp_4::model::LlamaModel;
2441    ///
2442    /// let path = LlamaModel::split_path("/models/llama", 1, 4);
2443    /// assert_eq!(path, "/models/llama-00002-of-00004.gguf");
2444    /// ```
2445    ///
2446    /// # Panics
2447    ///
2448    /// Panics if the path prefix contains a null byte.
2449    #[must_use]
2450    pub fn split_path(path_prefix: &str, split_no: i32, split_count: i32) -> String {
2451        let mut buffer = vec![0u8; 1024];
2452        let len = unsafe {
2453            llama_split_path(
2454                buffer.as_mut_ptr().cast::<c_char>(),
2455                buffer.len(),
2456                CString::new(path_prefix).unwrap().as_ptr(),
2457                split_no,
2458                split_count,
2459            )
2460        };
2461
2462        let len = usize::try_from(len).expect("split_path length fits in usize");
2463        buffer.truncate(len);
2464        String::from_utf8(buffer).unwrap_or_default()
2465    }
2466
2467    /// Extract the path prefix from a split filename.
2468    ///
2469    /// This function extracts the base path prefix from a split model filename,
2470    /// but only if the `split_no` and `split_count` match the pattern in the filename.
2471    ///
2472    /// # Arguments
2473    ///
2474    /// * `split_path` - The full path to the split file
2475    /// * `split_no` - The expected split number
2476    /// * `split_count` - The expected total number of splits
2477    ///
2478    /// # Returns
2479    ///
2480    /// Returns the path prefix if the pattern matches, or None if it doesn't
2481    ///
2482    /// # Example
2483    ///
2484    /// ```
2485    /// use llama_cpp_4::model::LlamaModel;
2486    ///
2487    /// let prefix = LlamaModel::split_prefix("/models/llama-00002-of-00004.gguf", 1, 4);
2488    /// assert_eq!(prefix, Some("/models/llama".to_string()));
2489    /// ```
2490    ///
2491    /// # Panics
2492    ///
2493    /// Panics if the split path contains a null byte.
2494    #[must_use]
2495    pub fn split_prefix(split_path: &str, split_no: i32, split_count: i32) -> Option<String> {
2496        let mut buffer = vec![0u8; 1024];
2497        let len = unsafe {
2498            llama_split_prefix(
2499                buffer.as_mut_ptr().cast::<c_char>(),
2500                buffer.len(),
2501                CString::new(split_path).unwrap().as_ptr(),
2502                split_no,
2503                split_count,
2504            )
2505        };
2506
2507        if len > 0 {
2508            let len = usize::try_from(len).expect("split_prefix length fits in usize");
2509            buffer.truncate(len);
2510            String::from_utf8(buffer).ok()
2511        } else {
2512            None
2513        }
2514    }
2515}
2516
2517#[allow(clippy::cast_precision_loss)]
2518impl fmt::Display for LlamaModel {
2519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2520        let desc = self.desc(256).unwrap_or_else(|_| "unknown".to_string());
2521        write!(
2522            f,
2523            "{desc} | {layers}L {heads}H {embd}E | {params} params | {size:.1} MiB",
2524            layers = self.n_layer(),
2525            heads = self.n_head(),
2526            embd = self.n_embd(),
2527            params = self.n_params(),
2528            size = self.model_size() as f64 / (1024.0 * 1024.0),
2529        )
2530    }
2531}
2532
2533impl Drop for LlamaModel {
2534    fn drop(&mut self) {
2535        unsafe { llama_model_free(self.model.as_ptr()) }
2536    }
2537}
2538
2539/// Defines the possible types of vocabulary used by the model.
2540///
2541/// The model may use different types of vocabulary depending on the tokenization method chosen during training.
2542/// This enum represents these types, specifically `BPE` (Byte Pair Encoding) and `SPM` (`SentencePiece`).
2543///
2544/// # Variants
2545///
2546/// - `BPE`: Byte Pair Encoding, a common tokenization method used in NLP tasks.
2547/// - `SPM`: `SentencePiece`, another popular tokenization method for NLP models.
2548///
2549/// # Example
2550///
2551/// ```no_run
2552/// use llama_cpp_4::model::VocabType;
2553///
2554/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2555/// let vocab_type = VocabType::BPE;
2556/// match vocab_type {
2557///     VocabType::BPE => println!("The model uses Byte Pair Encoding (BPE)"),
2558///     VocabType::SPM => println!("The model uses SentencePiece (SPM)"),
2559/// }
2560/// # Ok(())
2561/// # }
2562/// ```
2563#[repr(u32)]
2564#[derive(Debug, Eq, Copy, Clone, PartialEq)]
2565pub enum VocabType {
2566    /// Byte Pair Encoding
2567    BPE = LLAMA_VOCAB_TYPE_BPE as _,
2568    /// Sentence Piece Tokenizer
2569    SPM = LLAMA_VOCAB_TYPE_SPM as _,
2570}
2571
2572/// Error that occurs when trying to convert a `llama_vocab_type` to a `VocabType`.
2573///
2574/// This error is raised when the integer value returned by the system does not correspond to a known vocabulary type.
2575///
2576/// # Variants
2577///
2578/// - `UnknownValue`: The error is raised when the value is not a valid `llama_vocab_type`. The invalid value is returned with the error.
2579///
2580/// # Example
2581///
2582/// ```no_run
2583/// use llama_cpp_4::model::LlamaTokenTypeFromIntError;
2584///
2585/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2586/// let invalid_value = 999; // Not a valid vocabulary type
2587/// let error = LlamaTokenTypeFromIntError::UnknownValue(invalid_value);
2588/// println!("Error: {}", error);
2589/// # Ok(())
2590/// # }
2591/// ```
2592#[derive(thiserror::Error, Debug, Eq, PartialEq)]
2593pub enum LlamaTokenTypeFromIntError {
2594    /// The value is not a valid `llama_token_type`. Contains the int value that was invalid.
2595    #[error("Unknown Value {0}")]
2596    UnknownValue(llama_vocab_type),
2597}
2598
2599impl TryFrom<llama_vocab_type> for VocabType {
2600    type Error = LlamaTokenTypeFromIntError;
2601
2602    fn try_from(value: llama_vocab_type) -> Result<Self, Self::Error> {
2603        match value {
2604            LLAMA_VOCAB_TYPE_BPE => Ok(VocabType::BPE),
2605            LLAMA_VOCAB_TYPE_SPM => Ok(VocabType::SPM),
2606            unknown => Err(LlamaTokenTypeFromIntError::UnknownValue(unknown)),
2607        }
2608    }
2609}