ik_llama_cpp_2/model.rs
1//! Safe wrapper around `llama_model` ([`LlamaModel`]).
2
3pub mod chat;
4pub mod lora;
5pub mod meta;
6pub mod params;
7
8use std::ffi::CString;
9use std::num::NonZeroU16;
10use std::os::raw::c_char;
11use std::path::Path;
12use std::ptr::NonNull;
13
14use ik_llama_cpp_sys as sys;
15
16use crate::llama_backend::LlamaBackend;
17use crate::token::LlamaToken;
18use crate::LlamaError;
19
20pub use params::LlamaModelParams;
21
22/// A loaded ik_llama.cpp model.
23///
24/// Uses ik's model-pointer tokenizer/vocab API (`llama_tokenize(model, …)`,
25/// `llama_token_bos(model)`, …), which differs from modern stock llama.cpp's
26/// vocab-pointer API.
27#[derive(Debug)]
28pub struct LlamaModel {
29 pub(crate) model: NonNull<sys::llama_model>,
30}
31
32// SAFETY: after loading, the model is immutable and safe to share across threads
33// (matches llama-cpp-2's contract).
34unsafe impl Send for LlamaModel {}
35unsafe impl Sync for LlamaModel {}
36
37/// Whether to prepend a BOS (beginning-of-sequence) token when tokenizing.
38///
39/// Mirrors `llama-cpp-2`'s `model::AddBos`.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum AddBos {
42 /// Prepend the BOS token.
43 Always,
44 /// Do not prepend the BOS token.
45 Never,
46}
47
48impl LlamaModel {
49 /// Load a model from a single GGUF file (for split models, pass the merged
50 /// file or the first shard — see the crate docs on SPECIAL_SPLIT).
51 ///
52 /// # Invariant
53 ///
54 /// The [`LlamaBackend`] passed here (and any derived [`crate::LlamaContext`])
55 /// **must outlive** the returned model. Dropping the backend first runs
56 /// `llama_backend_free` before the model/context are freed, which is unsound.
57 /// The natural nested/reverse-drop order (backend → model → context declared
58 /// outer-to-inner, dropped inner-to-outer) satisfies this. (This mirrors the
59 /// `llama-cpp-2` anchor's contract.)
60 pub fn load_from_file(
61 _backend: &LlamaBackend,
62 path: impl AsRef<Path>,
63 params: &LlamaModelParams,
64 ) -> Result<Self, LlamaError> {
65 let path = path.as_ref();
66 let c_path = CString::new(path.to_str().ok_or(LlamaError::InvalidPath)?)?;
67 // SAFETY: valid C string + a fully-initialized params struct.
68 let raw = unsafe { sys::llama_model_load_from_file(c_path.as_ptr(), params.params) };
69 NonNull::new(raw)
70 .map(|model| Self { model })
71 .ok_or_else(|| LlamaError::ModelLoad(path.display().to_string()))
72 }
73
74 /// Number of vocabulary tokens.
75 #[must_use]
76 pub fn n_vocab(&self) -> i32 {
77 unsafe { sys::llama_n_vocab(self.model.as_ptr()) }
78 }
79
80 /// Number of MTP / NextN prediction layers (0 if the model has none).
81 #[must_use]
82 pub fn n_nextn_layer(&self) -> i32 {
83 unsafe { sys::llama_model_n_nextn_layer(self.model.as_ptr()) }
84 }
85
86 /// Beginning-of-sequence token.
87 #[must_use]
88 pub fn token_bos(&self) -> LlamaToken {
89 LlamaToken(unsafe { sys::llama_token_bos(self.model.as_ptr()) })
90 }
91
92 /// End-of-sequence token.
93 #[must_use]
94 pub fn token_eos(&self) -> LlamaToken {
95 LlamaToken(unsafe { sys::llama_token_eos(self.model.as_ptr()) })
96 }
97
98 /// Whether `token` marks end-of-generation (EOS/EOT/etc.).
99 #[must_use]
100 pub fn is_eog(&self, token: LlamaToken) -> bool {
101 unsafe { sys::llama_token_is_eog(self.model.as_ptr(), token.0) }
102 }
103
104 /// Whether `token` marks end-of-generation (alias of [`Self::is_eog`],
105 /// matching the `llama-cpp-2` method name).
106 #[must_use]
107 pub fn is_eog_token(&self, token: LlamaToken) -> bool {
108 self.is_eog(token)
109 }
110
111 /// Create a context for this model (matches `llama-cpp-2`'s
112 /// `model.new_context(&backend, params)`).
113 ///
114 /// The [`LlamaBackend`] argument is accepted for API parity; the returned
115 /// context borrows `self`, so the model must outlive it.
116 ///
117 /// # Errors
118 ///
119 /// [`LlamaError::ContextCreation`] if `llama_init_from_model` returns null.
120 pub fn new_context<'a>(
121 &'a self,
122 _backend: &LlamaBackend,
123 params: crate::context::params::LlamaContextParams,
124 ) -> Result<crate::context::LlamaContext<'a>, LlamaError> {
125 crate::context::LlamaContext::new(self, ¶ms)
126 }
127
128 /// Tokenize `text`, choosing whether to prepend BOS via [`AddBos`] (matches
129 /// `llama-cpp-2`'s `str_to_token`). Special tokens are parsed.
130 ///
131 /// # Errors
132 ///
133 /// [`LlamaError::Nul`] on an interior NUL byte; [`LlamaError::Tokenize`] if
134 /// tokenization fails.
135 pub fn str_to_token(&self, text: &str, add_bos: AddBos) -> Result<Vec<LlamaToken>, LlamaError> {
136 self.tokenize(text, matches!(add_bos, AddBos::Always))
137 }
138
139 /// Tokenize `text`. `add_bos` prepends the BOS token; special tokens are parsed.
140 pub fn tokenize(&self, text: &str, add_bos: bool) -> Result<Vec<LlamaToken>, LlamaError> {
141 let c_text = CString::new(text)?;
142 let text_len = text.len() as i32;
143 // Generous first guess; on overflow ik returns -(required).
144 let mut cap = (text.len() + 16) as i32;
145 let mut buf = vec![0 as sys::llama_token; cap as usize];
146 // SAFETY: buf has `cap` slots.
147 let mut n = unsafe {
148 sys::llama_tokenize(
149 self.model.as_ptr(),
150 c_text.as_ptr(),
151 text_len,
152 buf.as_mut_ptr(),
153 cap,
154 add_bos,
155 true,
156 )
157 };
158 if n < 0 {
159 cap = -n;
160 buf = vec![0 as sys::llama_token; cap as usize];
161 n = unsafe {
162 sys::llama_tokenize(
163 self.model.as_ptr(),
164 c_text.as_ptr(),
165 text_len,
166 buf.as_mut_ptr(),
167 cap,
168 add_bos,
169 true,
170 )
171 };
172 }
173 if n < 0 {
174 return Err(LlamaError::Tokenize);
175 }
176 buf.truncate(n as usize);
177 Ok(buf.into_iter().map(LlamaToken).collect())
178 }
179
180 /// Raw bytes of a single token's piece.
181 ///
182 /// `special` controls whether special/control tokens render as text;
183 /// `lstrip` strips that many leading spaces. `buf_size` is the initial
184 /// buffer guess — on overflow the call retries once with the exact size.
185 ///
186 /// # Errors
187 ///
188 /// [`LlamaError::TokenToPiece`] if the C conversion fails.
189 pub fn token_to_piece_bytes(
190 &self,
191 token: LlamaToken,
192 buf_size: usize,
193 special: bool,
194 lstrip: Option<NonZeroU16>,
195 ) -> Result<Vec<u8>, LlamaError> {
196 let lstrip_i = lstrip.map_or(0, |n| i32::from(n.get()));
197 let mut buf = vec![0u8; buf_size.max(1)];
198 let write = |buf: &mut [u8]| unsafe {
199 sys::llama_token_to_piece(
200 self.model.as_ptr(),
201 token.0,
202 buf.as_mut_ptr().cast::<c_char>(),
203 i32::try_from(buf.len()).unwrap_or(i32::MAX),
204 lstrip_i,
205 special,
206 )
207 };
208 let mut n = write(&mut buf);
209 if n < 0 {
210 buf = vec![0u8; (-n) as usize];
211 n = write(&mut buf);
212 if n < 0 {
213 return Err(LlamaError::TokenToPiece);
214 }
215 }
216 buf.truncate(n as usize);
217 Ok(buf)
218 }
219
220 /// Convert a single token to text, decoding its bytes incrementally through
221 /// `decoder` (so a multi-byte UTF-8 sequence split across token boundaries is
222 /// reassembled). Matches `llama-cpp-2`'s `token_to_piece`.
223 ///
224 /// `special` renders special/control tokens as text; `lstrip` strips leading
225 /// spaces. Use one decoder (`encoding_rs::UTF_8.new_decoder()`) per stream.
226 ///
227 /// A multi-byte sequence split at the very end of the stream stays buffered
228 /// in `decoder` (this never flushes with `last = true`); that matches the
229 /// `llama-cpp-2` anchor and is a non-issue for a stream ending on a token
230 /// boundary.
231 ///
232 /// # Errors
233 ///
234 /// [`LlamaError::TokenToPiece`] if the C conversion fails.
235 pub fn token_to_piece(
236 &self,
237 token: LlamaToken,
238 decoder: &mut encoding_rs::Decoder,
239 special: bool,
240 lstrip: Option<NonZeroU16>,
241 ) -> Result<String, LlamaError> {
242 let bytes = self.token_to_piece_bytes(token, 8, special, lstrip)?;
243 let mut out = String::with_capacity(bytes.len() + 16);
244 let _ = decoder.decode_to_string(&bytes, &mut out, false);
245 Ok(out)
246 }
247
248 /// Convert a single token to its text piece, lossily (UTF-8 replacement on
249 /// invalid bytes). Convenience over [`Self::token_to_piece`] for callers not
250 /// doing incremental streaming.
251 ///
252 /// # Errors
253 ///
254 /// [`LlamaError::TokenToPiece`] if the C conversion fails.
255 pub fn token_to_piece_lossy(&self, token: LlamaToken) -> Result<String, LlamaError> {
256 let bytes = self.token_to_piece_bytes(token, 8, false, None)?;
257 Ok(String::from_utf8_lossy(&bytes).into_owned())
258 }
259
260 /// Detokenize a slice of tokens into a string, decoding incrementally through
261 /// a single UTF-8 decoder (`special = false`).
262 ///
263 /// Note: this concatenates per-token pieces, so special tokens are dropped and
264 /// spacing may differ slightly from a true detokenizer. ik only exposes the
265 /// vocab-pointer `llama_detokenize` (the model-pointer overload is commented
266 /// out in the header); a full detokenizer path is a follow-up.
267 pub fn detokenize(&self, tokens: &[LlamaToken]) -> Result<String, LlamaError> {
268 let mut decoder = encoding_rs::UTF_8.new_decoder();
269 let mut s = String::new();
270 for &t in tokens {
271 s.push_str(&self.token_to_piece(t, &mut decoder, false, None)?);
272 }
273 Ok(s)
274 }
275}
276
277impl Drop for LlamaModel {
278 fn drop(&mut self) {
279 unsafe { sys::llama_free_model(self.model.as_ptr()) };
280 }
281}