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, ¶ms)?;
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}