Skip to main content

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, &params)
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::TokenOutOfRange`] if `token` is not in `[0, n_vocab)`;
189    /// [`LlamaError::TokenToPiece`] if the C conversion fails.
190    pub fn token_to_piece_bytes(
191        &self,
192        token: LlamaToken,
193        buf_size: usize,
194        special: bool,
195        lstrip: Option<NonZeroU16>,
196    ) -> Result<Vec<u8>, LlamaError> {
197        // ik's tokenizer does an out-of-range `cache.at(token)` (throws
198        // std::out_of_range) for a token outside `[0, n_vocab)`; that C++ throw
199        // would unwind across the `extern "C"` boundary and abort the process.
200        // Reject a bad id here so it is a normal error from safe code, not a crash.
201        let n_vocab = self.n_vocab();
202        if token.0 < 0 || token.0 >= n_vocab {
203            return Err(LlamaError::TokenOutOfRange {
204                token: token.0,
205                n_vocab,
206            });
207        }
208        let lstrip_i = lstrip.map_or(0, |n| i32::from(n.get()));
209        let mut buf = vec![0u8; buf_size.max(1)];
210        let write = |buf: &mut [u8]| unsafe {
211            sys::llama_token_to_piece(
212                self.model.as_ptr(),
213                token.0,
214                buf.as_mut_ptr().cast::<c_char>(),
215                i32::try_from(buf.len()).unwrap_or(i32::MAX),
216                lstrip_i,
217                special,
218            )
219        };
220        let mut n = write(&mut buf);
221        if n < 0 {
222            buf = vec![0u8; (-n) as usize];
223            n = write(&mut buf);
224            if n < 0 {
225                return Err(LlamaError::TokenToPiece);
226            }
227        }
228        buf.truncate(n as usize);
229        Ok(buf)
230    }
231
232    /// Convert a single token to text, decoding its bytes incrementally through
233    /// `decoder` (so a multi-byte UTF-8 sequence split across token boundaries is
234    /// reassembled). Matches `llama-cpp-2`'s `token_to_piece`.
235    ///
236    /// `special` renders special/control tokens as text; `lstrip` strips leading
237    /// spaces. Use one decoder (`encoding_rs::UTF_8.new_decoder()`) per stream.
238    ///
239    /// A multi-byte sequence split at the very end of the stream stays buffered
240    /// in `decoder` (this never flushes with `last = true`); that matches the
241    /// `llama-cpp-2` anchor and is a non-issue for a stream ending on a token
242    /// boundary.
243    ///
244    /// # Errors
245    ///
246    /// [`LlamaError::TokenToPiece`] if the C conversion fails.
247    pub fn token_to_piece(
248        &self,
249        token: LlamaToken,
250        decoder: &mut encoding_rs::Decoder,
251        special: bool,
252        lstrip: Option<NonZeroU16>,
253    ) -> Result<String, LlamaError> {
254        let bytes = self.token_to_piece_bytes(token, 8, special, lstrip)?;
255        let mut out = String::with_capacity(bytes.len() + 16);
256        let _ = decoder.decode_to_string(&bytes, &mut out, false);
257        Ok(out)
258    }
259
260    /// Convert a single token to its text piece, lossily (UTF-8 replacement on
261    /// invalid bytes). Convenience over [`Self::token_to_piece`] for callers not
262    /// doing incremental streaming.
263    ///
264    /// # Errors
265    ///
266    /// [`LlamaError::TokenToPiece`] if the C conversion fails.
267    pub fn token_to_piece_lossy(&self, token: LlamaToken) -> Result<String, LlamaError> {
268        let bytes = self.token_to_piece_bytes(token, 8, false, None)?;
269        Ok(String::from_utf8_lossy(&bytes).into_owned())
270    }
271
272    /// Detokenize a slice of tokens into a string, decoding incrementally through
273    /// a single UTF-8 decoder (`special = false`).
274    ///
275    /// Note: this concatenates per-token pieces, so special tokens are dropped and
276    /// spacing may differ slightly from a true detokenizer. ik only exposes the
277    /// vocab-pointer `llama_detokenize` (the model-pointer overload is commented
278    /// out in the header); a full detokenizer path is a follow-up.
279    pub fn detokenize(&self, tokens: &[LlamaToken]) -> Result<String, LlamaError> {
280        let mut decoder = encoding_rs::UTF_8.new_decoder();
281        let mut s = String::new();
282        for &t in tokens {
283            s.push_str(&self.token_to_piece(t, &mut decoder, false, None)?);
284        }
285        Ok(s)
286    }
287}
288
289impl Drop for LlamaModel {
290    fn drop(&mut self) {
291        unsafe { sys::llama_free_model(self.model.as_ptr()) };
292    }
293}