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