1use std::ffi::CStr;
3use std::ffi::CString;
4use std::num::NonZeroU16;
5use std::os::raw::c_int;
6use std::path::Path;
7use std::ptr::NonNull;
8
9use crate::context::params::LlamaContextParams;
10use crate::context::LlamaContext;
11use crate::llama_backend::LlamaBackend;
12use crate::model::params::LlamaModelParams;
13use crate::token::LlamaToken;
14use crate::token_type::{LlamaTokenAttr, LlamaTokenAttrs};
15use crate::{
16 ApplyChatTemplateError, ChatTemplateError, LlamaContextLoadError, LlamaLoraAdapterInitError,
17 LlamaModelLoadError, NewLlamaChatMessageError, StringToTokenError, TokenToStringError,
18};
19
20pub mod params;
21
22#[derive(Debug)]
24#[repr(transparent)]
25#[allow(clippy::module_name_repetitions)]
26pub struct LlamaModel {
27 pub(crate) model: NonNull<llama_cpp_sys_2::llama_model>,
28}
29
30#[derive(Debug)]
32#[repr(transparent)]
33#[allow(clippy::module_name_repetitions)]
34pub struct LlamaLoraAdapter {
35 pub(crate) lora_adapter: NonNull<llama_cpp_sys_2::llama_lora_adapter>,
36}
37
38#[derive(Debug, Eq, PartialEq, Clone)]
40pub struct LlamaChatMessage {
41 role: CString,
42 content: CString,
43}
44
45impl LlamaChatMessage {
46 pub fn new(role: String, content: String) -> Result<Self, NewLlamaChatMessageError> {
48 Ok(Self {
49 role: CString::new(role)?,
50 content: CString::new(content)?,
51 })
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum AddBos {
58 Always,
60 Never,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Special {
67 Tokenize,
69 Plaintext,
71}
72
73unsafe impl Send for LlamaModel {}
74
75unsafe impl Sync for LlamaModel {}
76
77impl LlamaModel {
78 #[must_use]
85 pub fn n_ctx_train(&self) -> u32 {
86 let n_ctx_train = unsafe { llama_cpp_sys_2::llama_n_ctx_train(self.model.as_ptr()) };
87 u32::try_from(n_ctx_train).expect("n_ctx_train fits into an u32")
88 }
89
90 pub fn tokens(
92 &self,
93 special: Special,
94 ) -> impl Iterator<Item = (LlamaToken, Result<String, TokenToStringError>)> + '_ {
95 (0..self.n_vocab())
96 .map(LlamaToken::new)
97 .map(move |llama_token| (llama_token, self.token_to_str(llama_token, special)))
98 }
99
100 #[must_use]
102 pub fn token_bos(&self) -> LlamaToken {
103 let token = unsafe { llama_cpp_sys_2::llama_token_bos(self.model.as_ptr()) };
104 LlamaToken(token)
105 }
106
107 #[must_use]
109 pub fn token_eos(&self) -> LlamaToken {
110 let token = unsafe { llama_cpp_sys_2::llama_token_eos(self.model.as_ptr()) };
111 LlamaToken(token)
112 }
113
114 #[must_use]
116 pub fn token_nl(&self) -> LlamaToken {
117 let token = unsafe { llama_cpp_sys_2::llama_token_nl(self.model.as_ptr()) };
118 LlamaToken(token)
119 }
120
121 #[must_use]
123 pub fn is_eog_token(&self, token: LlamaToken) -> bool {
124 unsafe { llama_cpp_sys_2::llama_token_is_eog(self.model.as_ptr(), token.0) }
125 }
126
127 #[must_use]
129 pub fn decode_start_token(&self) -> LlamaToken {
130 let token =
131 unsafe { llama_cpp_sys_2::llama_model_decoder_start_token(self.model.as_ptr()) };
132 LlamaToken(token)
133 }
134
135 pub fn token_to_str(
141 &self,
142 token: LlamaToken,
143 special: Special,
144 ) -> Result<String, TokenToStringError> {
145 self.token_to_str_with_size(token, 32, special)
146 }
147
148 pub fn token_to_bytes(
154 &self,
155 token: LlamaToken,
156 special: Special,
157 ) -> Result<Vec<u8>, TokenToStringError> {
158 self.token_to_bytes_with_size(token, 32, special, None)
159 }
160
161 pub fn tokens_to_str(
167 &self,
168 tokens: &[LlamaToken],
169 special: Special,
170 ) -> Result<String, TokenToStringError> {
171 let mut builder = String::with_capacity(tokens.len() * 4);
172 for str in tokens
173 .iter()
174 .copied()
175 .map(|t| self.token_to_str(t, special))
176 {
177 builder += &str?;
178 }
179 Ok(builder)
180 }
181
182 pub fn str_to_token(
205 &self,
206 str: &str,
207 add_bos: AddBos,
208 ) -> Result<Vec<LlamaToken>, StringToTokenError> {
209 let add_bos = match add_bos {
210 AddBos::Always => true,
211 AddBos::Never => false,
212 };
213
214 let tokens_estimation = std::cmp::max(8, (str.len() / 2) + usize::from(add_bos));
215 let mut buffer = Vec::with_capacity(tokens_estimation);
216
217 let c_string = CString::new(str)?;
218 let buffer_capacity =
219 c_int::try_from(buffer.capacity()).expect("buffer capacity should fit into a c_int");
220
221 let size = unsafe {
222 llama_cpp_sys_2::llama_tokenize(
223 self.model.as_ptr(),
224 c_string.as_ptr(),
225 c_int::try_from(c_string.as_bytes().len())?,
226 buffer.as_mut_ptr(),
227 buffer_capacity,
228 add_bos,
229 true,
230 )
231 };
232
233 let size = if size.is_negative() {
236 buffer.reserve_exact(usize::try_from(-size).expect("usize's are larger "));
237 unsafe {
238 llama_cpp_sys_2::llama_tokenize(
239 self.model.as_ptr(),
240 c_string.as_ptr(),
241 c_int::try_from(c_string.as_bytes().len())?,
242 buffer.as_mut_ptr(),
243 -size,
244 add_bos,
245 true,
246 )
247 }
248 } else {
249 size
250 };
251
252 let size = usize::try_from(size).expect("size is positive and usize ");
253
254 unsafe { buffer.set_len(size) }
256 Ok(buffer.into_iter().map(LlamaToken).collect())
257 }
258
259 #[must_use]
265 pub fn token_attr(&self, LlamaToken(id): LlamaToken) -> LlamaTokenAttrs {
266 let token_type = unsafe { llama_cpp_sys_2::llama_token_get_attr(self.model.as_ptr(), id) };
267 LlamaTokenAttrs::try_from(token_type).expect("token type is valid")
268 }
269
270 pub fn token_to_str_with_size(
286 &self,
287 token: LlamaToken,
288 buffer_size: usize,
289 special: Special,
290 ) -> Result<String, TokenToStringError> {
291 let bytes = self.token_to_bytes_with_size(token, buffer_size, special, None)?;
292 Ok(String::from_utf8(bytes)?)
293 }
294
295 pub fn token_to_bytes_with_size(
310 &self,
311 token: LlamaToken,
312 buffer_size: usize,
313 special: Special,
314 lstrip: Option<NonZeroU16>,
315 ) -> Result<Vec<u8>, TokenToStringError> {
316 if token == self.token_nl() {
317 return Ok(String::from("\n").into_bytes());
318 }
319
320 let attrs = self.token_attr(token);
322 if attrs.contains(LlamaTokenAttr::Control)
323 && (token == self.token_bos() || token == self.token_eos())
324 {
325 return Ok(Vec::new());
326 } else if attrs.is_empty()
327 || attrs
328 .intersects(LlamaTokenAttr::Unknown | LlamaTokenAttr::Byte | LlamaTokenAttr::Unused)
329 {
330 return Ok(Vec::new());
331 }
332
333 let special = match special {
334 Special::Tokenize => true,
335 Special::Plaintext => false,
336 };
337
338 let string = CString::new(vec![b'*'; buffer_size]).expect("no null");
339 let len = string.as_bytes().len();
340 let len = c_int::try_from(len).expect("length fits into c_int");
341 let buf = string.into_raw();
342 let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
343 let size = unsafe {
344 llama_cpp_sys_2::llama_token_to_piece(
345 self.model.as_ptr(),
346 token.0,
347 buf,
348 len,
349 lstrip,
350 special,
351 )
352 };
353
354 match size {
355 0 => Err(TokenToStringError::UnknownTokenType),
356 i if i.is_negative() => Err(TokenToStringError::InsufficientBufferSpace(i)),
357 size => {
358 let string = unsafe { CString::from_raw(buf) };
359 let mut bytes = string.into_bytes();
360 let len = usize::try_from(size).expect("size is positive and fits into usize");
361 bytes.truncate(len);
362 Ok(bytes)
363 }
364 }
365 }
366 #[must_use]
371 pub fn n_vocab(&self) -> i32 {
372 unsafe { llama_cpp_sys_2::llama_n_vocab(self.model.as_ptr()) }
373 }
374
375 #[must_use]
381 pub fn vocab_type(&self) -> VocabType {
382 let vocab_type = unsafe { llama_cpp_sys_2::llama_vocab_type(self.model.as_ptr()) };
383 VocabType::try_from(vocab_type).expect("invalid vocab type")
384 }
385
386 #[must_use]
389 pub fn n_embd(&self) -> c_int {
390 unsafe { llama_cpp_sys_2::llama_n_embd(self.model.as_ptr()) }
391 }
392
393 #[allow(clippy::missing_panics_doc)] pub fn get_chat_template(&self, buf_size: usize) -> Result<String, ChatTemplateError> {
401 let chat_temp = CString::new(vec![b'*'; buf_size]).expect("no null");
403 let chat_ptr = chat_temp.into_raw();
404 let chat_name = CString::new("tokenizer.chat_template").expect("no null bytes");
405
406 let ret = unsafe {
407 llama_cpp_sys_2::llama_model_meta_val_str(
408 self.model.as_ptr(),
409 chat_name.as_ptr(),
410 chat_ptr,
411 buf_size,
412 )
413 };
414
415 if ret < 0 {
416 return Err(ChatTemplateError::MissingTemplate(ret));
417 }
418
419 let template_c = unsafe { CString::from_raw(chat_ptr) };
420 let template = template_c.to_str()?;
421
422 let ret: usize = ret.try_into().unwrap();
423 if template.len() < ret {
424 return Err(ChatTemplateError::BuffSizeError(ret + 1));
425 }
426
427 Ok(template.to_owned())
428 }
429
430 #[tracing::instrument(skip_all, fields(params))]
436 pub fn load_from_file(
437 _: &LlamaBackend,
438 path: impl AsRef<Path>,
439 params: &LlamaModelParams,
440 ) -> Result<Self, LlamaModelLoadError> {
441 let path = path.as_ref();
442 debug_assert!(Path::new(path).exists(), "{path:?} does not exist");
443 let path = path
444 .to_str()
445 .ok_or(LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
446
447 let cstr = CString::new(path)?;
448 let llama_model =
449 unsafe { llama_cpp_sys_2::llama_load_model_from_file(cstr.as_ptr(), params.params) };
450
451 let model = NonNull::new(llama_model).ok_or(LlamaModelLoadError::NullResult)?;
452
453 tracing::debug!(?path, "Loaded model");
454 Ok(LlamaModel { model })
455 }
456
457 pub fn lora_adapter_init(
463 &self,
464 path: impl AsRef<Path>,
465 ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
466 let path = path.as_ref();
467 debug_assert!(Path::new(path).exists(), "{path:?} does not exist");
468
469 let path = path
470 .to_str()
471 .ok_or(LlamaLoraAdapterInitError::PathToStrError(
472 path.to_path_buf(),
473 ))?;
474
475 let cstr = CString::new(path)?;
476 let adapter =
477 unsafe { llama_cpp_sys_2::llama_lora_adapter_init(self.model.as_ptr(), cstr.as_ptr()) };
478
479 let adapter = NonNull::new(adapter).ok_or(LlamaLoraAdapterInitError::NullResult)?;
480
481 tracing::debug!(?path, "Initialized lora adapter");
482 Ok(LlamaLoraAdapter {
483 lora_adapter: adapter,
484 })
485 }
486
487 #[allow(clippy::needless_pass_by_value)]
494 pub fn new_context(
495 &self,
496 _: &LlamaBackend,
497 params: LlamaContextParams,
498 ) -> Result<LlamaContext, LlamaContextLoadError> {
499 let context_params = params.context_params;
500 let context = unsafe {
501 llama_cpp_sys_2::llama_new_context_with_model(self.model.as_ptr(), context_params)
502 };
503 let context = NonNull::new(context).ok_or(LlamaContextLoadError::NullReturn)?;
504
505 Ok(LlamaContext::new(self, context, params.embeddings()))
506 }
507
508 #[tracing::instrument(skip_all)]
516 pub fn apply_chat_template(
517 &self,
518 tmpl: Option<String>,
519 chat: Vec<LlamaChatMessage>,
520 add_ass: bool,
521 ) -> Result<String, ApplyChatTemplateError> {
522 let message_length = chat.iter().fold(0, |acc, c| {
524 acc + c.role.to_bytes().len() + c.content.to_bytes().len()
525 });
526 let mut buff = vec![0; message_length * 4];
527
528 let chat: Vec<llama_cpp_sys_2::llama_chat_message> = chat
530 .iter()
531 .map(|c| llama_cpp_sys_2::llama_chat_message {
532 role: c.role.as_ptr(),
533 content: c.content.as_ptr(),
534 })
535 .collect();
536
537 let tmpl = tmpl.map(CString::new);
539 let tmpl_ptr = match &tmpl {
540 Some(str) => str.as_ref().map_err(Clone::clone)?.as_ptr(),
541 None => std::ptr::null(),
542 };
543
544 let formatted_chat = unsafe {
545 let res = llama_cpp_sys_2::llama_chat_apply_template(
546 self.model.as_ptr(),
547 tmpl_ptr,
548 chat.as_ptr(),
549 chat.len(),
550 add_ass,
551 buff.as_mut_ptr(),
552 buff.len() as i32,
553 );
554 if res > buff.len() as i32 {
557 return Err(ApplyChatTemplateError::BuffSizeError);
558 }
559 Ok::<String, ApplyChatTemplateError>(
560 CStr::from_ptr(buff.as_mut_ptr())
561 .to_string_lossy()
562 .to_string(),
563 )
564 }?;
565 Ok(formatted_chat)
566 }
567}
568
569impl Drop for LlamaModel {
570 fn drop(&mut self) {
571 unsafe { llama_cpp_sys_2::llama_free_model(self.model.as_ptr()) }
572 }
573}
574
575#[repr(u32)]
577#[derive(Debug, Eq, Copy, Clone, PartialEq)]
578pub enum VocabType {
579 BPE = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_BPE as _,
581 SPM = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_SPM as _,
583}
584
585#[derive(thiserror::Error, Debug, Eq, PartialEq)]
587pub enum LlamaTokenTypeFromIntError {
588 #[error("Unknown Value {0}")]
590 UnknownValue(llama_cpp_sys_2::llama_vocab_type),
591}
592
593impl TryFrom<llama_cpp_sys_2::llama_vocab_type> for VocabType {
594 type Error = LlamaTokenTypeFromIntError;
595
596 fn try_from(value: llama_cpp_sys_2::llama_vocab_type) -> Result<Self, Self::Error> {
597 match value {
598 llama_cpp_sys_2::LLAMA_VOCAB_TYPE_BPE => Ok(VocabType::BPE),
599 llama_cpp_sys_2::LLAMA_VOCAB_TYPE_SPM => Ok(VocabType::SPM),
600 unknown => Err(LlamaTokenTypeFromIntError::UnknownValue(unknown)),
601 }
602 }
603}