Skip to main content

llama_cpp_bindings/
model.rs

1pub mod add_bos;
2pub mod llama_chat_message;
3pub mod llama_chat_template;
4pub mod llama_lora_adapter;
5pub mod llama_split_mode_parse_error;
6pub mod params;
7pub mod rope_type;
8pub mod split_mode;
9pub mod vocab_type;
10pub mod vocab_type_from_int_error;
11
12use std::ffi::{CStr, CString, c_char};
13use std::num::NonZeroU16;
14use std::os::raw::c_int;
15use std::path::Path;
16use std::ptr;
17use std::ptr::NonNull;
18use std::sync::Arc;
19use std::sync::OnceLock;
20
21use toktrie::ApproximateTokEnv;
22use toktrie::TokRxInfo;
23use toktrie::TokTrie;
24
25use llama_cpp_bindings_types::ParsedChatMessage;
26use llama_cpp_bindings_types::ParsedToolCall;
27use llama_cpp_bindings_types::ReasoningMarkers;
28use llama_cpp_bindings_types::ToolCallArguments;
29use llama_cpp_bindings_types::ToolCallMarkers;
30
31use crate::chat_message_parse_outcome::ChatMessageParseOutcome;
32use crate::llama_backend::LlamaBackend;
33use crate::llama_token_attrs::LlamaTokenAttrs;
34use crate::llama_token_attrs_from_int_error::LlamaTokenAttrsFromIntError;
35use crate::raw_chat_message::RawChatMessage;
36use crate::resolved_tool_call_markers::ResolvedToolCallMarkers;
37use crate::sampled_token::SampledToken;
38use crate::sampled_token_classifier::SampledTokenClassifier;
39use crate::streaming_markers::StreamingMarkers;
40use crate::token::LlamaToken;
41use crate::tool_call_format;
42use crate::tool_call_format::ToolCallFormatOutcome;
43use crate::tool_call_template_overrides;
44use crate::{
45    ApplyChatTemplateError, ChatTemplateError, LlamaLoraAdapterInitError, LlamaModelLoadError,
46    MarkerDetectionError, MetaValError, ParseChatMessageError, StringToTokenError,
47    TokenToStringError,
48};
49
50pub use add_bos::AddBos;
51pub use llama_chat_message::LlamaChatMessage;
52pub use llama_chat_template::LlamaChatTemplate;
53pub use llama_lora_adapter::LlamaLoraAdapter;
54pub use rope_type::RopeType;
55pub use vocab_type::VocabType;
56pub use vocab_type_from_int_error::VocabTypeFromIntError;
57
58use params::LlamaModelParams;
59
60fn validate_string_length_for_tokenizer(length: usize) -> Result<c_int, StringToTokenError> {
61    Ok(c_int::try_from(length)?)
62}
63
64fn cstring_with_validated_len(str: &str) -> Result<(CString, c_int), StringToTokenError> {
65    let c_string = CString::new(str)?;
66    let len = validate_string_length_for_tokenizer(c_string.as_bytes().len())?;
67    Ok((c_string, len))
68}
69
70pub struct LlamaModel {
71    pub model: NonNull<llama_cpp_bindings_sys::llama_model>,
72    tok_env: OnceLock<Arc<ApproximateTokEnv>>,
73    chat_parser: OnceLock<ChatParserHandle>,
74}
75
76#[derive(Debug)]
77struct ChatParserHandle {
78    parser: NonNull<llama_cpp_bindings_sys::llama_rs_chat_parser>,
79}
80
81// SAFETY: the handle is an opaque pointer to a heap-allocated parser owned by the
82// model; it is created once, never mutated afterwards, and freed exactly once on
83// drop. The owning `LlamaModel` is already `Send + Sync`, so the handle shares that
84// guarantee.
85unsafe impl Send for ChatParserHandle {}
86
87unsafe impl Sync for ChatParserHandle {}
88
89impl Drop for ChatParserHandle {
90    fn drop(&mut self) {
91        let mut out_error: *mut c_char = ptr::null_mut();
92        unsafe {
93            llama_cpp_bindings_sys::llama_rs_chat_parser_free(
94                self.parser.as_ptr(),
95                &raw mut out_error,
96            );
97        }
98        if !out_error.is_null() {
99            let _ = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
100        }
101    }
102}
103
104impl std::fmt::Debug for LlamaModel {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("LlamaModel")
107            .field("model", &self.model)
108            .finish_non_exhaustive()
109    }
110}
111
112unsafe impl Send for LlamaModel {}
113
114unsafe impl Sync for LlamaModel {}
115
116// SAFETY: `out_model` and `out_error` must be the pointers populated by the
117// preceding `llama_rs_load_model_from_file` call (or null); `out_error` is read
118// and freed only in the CXX-exception arm.
119unsafe fn load_model_from_file_status_to_result(
120    status: llama_cpp_bindings_sys::llama_rs_load_model_from_file_status,
121    out_model: *mut llama_cpp_bindings_sys::llama_model,
122    out_error: *mut c_char,
123    path: &Path,
124) -> Result<LlamaModel, LlamaModelLoadError> {
125    match status {
126        llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_OK => {
127            let model = NonNull::new(out_model).ok_or(LlamaModelLoadError::Unloadable)?;
128            Ok(LlamaModel {
129                model,
130                tok_env: OnceLock::new(),
131                chat_parser: OnceLock::new(),
132            })
133        }
134        llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_VENDORED_RETURNED_NULL => {
135            if path.exists() {
136                Err(LlamaModelLoadError::Unloadable)
137            } else {
138                Err(LlamaModelLoadError::FileNotFound(path.to_path_buf()))
139            }
140        }
141        llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_ERROR_STRING_ALLOCATION_FAILED => {
142            Err(LlamaModelLoadError::NotEnoughMemory)
143        }
144        llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_VENDORED_THREW_CXX_EXCEPTION => {
145            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
146            Err(LlamaModelLoadError::Reported { message })
147        }
148        other => {
149            unreachable!("llama_rs_load_model_from_file returned unrecognized status {other}")
150        }
151    }
152}
153
154// SAFETY: `handle` must be the parsed-chat handle (or null) and `out_error` must
155// reference the pointer populated by the preceding `llama_rs_parse_chat_message`
156// call. In the CXX-exception arm the error is read, freed, and the referenced
157// pointer is nulled so the later free in the caller does not double-free.
158unsafe fn parse_chat_message_status_to_result(
159    status: llama_cpp_bindings_sys::llama_rs_parse_chat_message_status,
160    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
161    out_error: *mut *mut c_char,
162) -> Result<ParsedChatMessage, ParseChatMessageError> {
163    match status {
164        llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_OK => {
165            collect_parsed_chat_message(handle)
166        }
167        llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_ERROR_STRING_ALLOCATION_FAILED => {
168            Err(ParseChatMessageError::NotEnoughMemory)
169        }
170        llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_VENDORED_THREW_CXX_EXCEPTION => {
171            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(*out_error) };
172            unsafe { *out_error = ptr::null_mut() };
173            Err(ParseChatMessageError::ParseFailed { message })
174        }
175        other => {
176            unreachable!("llama_rs_parse_chat_message returned unrecognized status {other}")
177        }
178    }
179}
180
181// SAFETY: `out_error` must reference the pointer populated by the preceding
182// `llama_rs_chat_parser_create` call (or null); it is read, freed, and nulled only in
183// the CXX-exception arm. `parser` must be the pointer populated by the same call.
184unsafe fn chat_parser_create_status_to_result(
185    status: llama_cpp_bindings_sys::llama_rs_chat_parser_create_status,
186    parser: *mut llama_cpp_bindings_sys::llama_rs_chat_parser,
187    out_error: *mut *mut c_char,
188) -> Result<ChatParserHandle, ParseChatMessageError> {
189    match status {
190        llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_OK => NonNull::new(parser).map_or_else(
191            || unreachable!("llama_rs_chat_parser_create returned OK with a null parser handle"),
192            |parser| Ok(ChatParserHandle { parser }),
193        ),
194        llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_MODEL_HAS_NO_CHAT_TEMPLATE => {
195            Err(ParseChatMessageError::NoChatTemplate)
196        }
197        llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_MODEL_HAS_NO_VOCAB => {
198            Err(ParseChatMessageError::NoVocab)
199        }
200        llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_ERROR_STRING_ALLOCATION_FAILED => {
201            Err(ParseChatMessageError::NotEnoughMemory)
202        }
203        llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_VENDORED_THREW_CXX_EXCEPTION => {
204            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(*out_error) };
205            unsafe { *out_error = ptr::null_mut() };
206            Err(ParseChatMessageError::ParseFailed { message })
207        }
208        other => {
209            unreachable!("llama_rs_chat_parser_create returned unrecognized status {other}")
210        }
211    }
212}
213
214// SAFETY: `out_error` and `free_error` must be the pointers populated by the
215// preceding parse and `llama_rs_parsed_chat_free` calls (or null); every arm
216// frees each pointer exactly once across the two `llama_rs_string_free` calls.
217unsafe fn parsed_chat_free_status_to_result(
218    parsed: Result<ParsedChatMessage, ParseChatMessageError>,
219    free_status: llama_cpp_bindings_sys::llama_rs_parsed_chat_free_status,
220    out_error: *mut c_char,
221    free_error: *mut c_char,
222) -> Result<ParsedChatMessage, ParseChatMessageError> {
223    match (parsed, free_status) {
224        (Ok(value), llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_OK) => {
225            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
226            Ok(value)
227        }
228        (
229            Ok(_),
230            llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_DESTRUCTOR_THREW_CXX_EXCEPTION,
231        ) => {
232            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
233            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(free_error) };
234            Err(ParseChatMessageError::DestructorFailed { message })
235        }
236        (
237            Ok(_),
238            llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_ERROR_STRING_ALLOCATION_FAILED,
239        ) => {
240            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
241            Err(ParseChatMessageError::NotEnoughMemory)
242        }
243        (Ok(_), other) => {
244            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
245            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(free_error) };
246            unreachable!("llama_rs_parsed_chat_free returned unrecognized status {other}")
247        }
248        (Err(parse_err), _) => {
249            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
250            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(free_error) };
251            Err(parse_err)
252        }
253    }
254}
255
256fn reasoning_markers_from_marker_pair(
257    open: Option<String>,
258    close: Option<String>,
259) -> Option<ReasoningMarkers> {
260    match (open, close) {
261        (Some(open), Some(close)) if !open.is_empty() && !close.is_empty() => {
262            Some(ReasoningMarkers { open, close })
263        }
264        _ => None,
265    }
266}
267
268fn outcome_from_via_ffi_result(
269    via_ffi_result: Result<ParsedChatMessage, ParseChatMessageError>,
270    tools_json: &str,
271    input: &str,
272    is_partial: bool,
273) -> Result<ChatMessageParseOutcome, ParseChatMessageError> {
274    match via_ffi_result {
275        Ok(mut parsed) => {
276            synthesize_missing_tool_call_ids(&mut parsed.tool_calls);
277            Ok(ChatMessageParseOutcome::Recognized(parsed))
278        }
279        Err(ParseChatMessageError::ParseFailed { message }) => {
280            Ok(ChatMessageParseOutcome::Unrecognized(RawChatMessage {
281                tools_json: tools_json.to_owned(),
282                text: input.to_owned(),
283                is_partial,
284                ffi_error_message: message,
285            }))
286        }
287        Err(other) => Err(other),
288    }
289}
290
291// SAFETY: `out_string` and `out_error` must be the pointers populated by the
292// preceding `llama_rs_apply_chat_template` call (or null). The success arm reads
293// and frees `out_string`; the CXX-exception arm reads and frees `out_error`.
294unsafe fn apply_chat_template_status_to_result(
295    status: llama_cpp_bindings_sys::llama_rs_apply_chat_template_status,
296    out_string: *mut c_char,
297    out_error: *mut c_char,
298) -> Result<String, ApplyChatTemplateError> {
299    match status {
300        llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_OK => {
301            Ok(unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_string) })
302        }
303        llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_MODEL_HAS_NO_VOCAB => {
304            Err(ApplyChatTemplateError::NoVocab)
305        }
306        llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_TEMPLATE_APPLICATION_FAILED => {
307            Err(ApplyChatTemplateError::TemplateApplicationFailed)
308        }
309        llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_ERROR_STRING_ALLOCATION_FAILED => {
310            Err(ApplyChatTemplateError::NotEnoughMemory)
311        }
312        llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_VENDORED_THREW_CXX_EXCEPTION => {
313            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
314            Err(ApplyChatTemplateError::Reported { message })
315        }
316        other => {
317            unreachable!("llama_rs_apply_chat_template returned unrecognized status {other}")
318        }
319    }
320}
321
322impl LlamaModel {
323    #[must_use]
324    pub fn vocab_ptr(&self) -> *const llama_cpp_bindings_sys::llama_vocab {
325        unsafe { llama_cpp_bindings_sys::llama_model_get_vocab(self.model.as_ptr()) }
326    }
327
328    /// # Errors
329    ///
330    /// Returns an error if the value returned by llama.cpp does not fit into a `u32`.
331    pub fn n_ctx_train(&self) -> Result<u32, std::num::TryFromIntError> {
332        let n_ctx_train = unsafe { llama_cpp_bindings_sys::llama_n_ctx_train(self.model.as_ptr()) };
333
334        u32::try_from(n_ctx_train)
335    }
336
337    pub fn tokens(
338        &self,
339        decode_special: bool,
340    ) -> impl Iterator<Item = (LlamaToken, Result<String, TokenToStringError>)> + '_ {
341        (0..self.n_vocab())
342            .map(LlamaToken::new)
343            .map(move |llama_token| {
344                let mut decoder = encoding_rs::UTF_8.new_decoder();
345                (
346                    llama_token,
347                    self.token_to_piece(
348                        &SampledToken::Content(llama_token),
349                        &mut decoder,
350                        decode_special,
351                        None,
352                    ),
353                )
354            })
355    }
356
357    #[must_use]
358    pub fn token_bos(&self) -> LlamaToken {
359        let token = unsafe { llama_cpp_bindings_sys::llama_token_bos(self.vocab_ptr()) };
360        LlamaToken(token)
361    }
362
363    #[must_use]
364    pub fn token_eos(&self) -> LlamaToken {
365        let token = unsafe { llama_cpp_bindings_sys::llama_token_eos(self.vocab_ptr()) };
366        LlamaToken(token)
367    }
368
369    #[must_use]
370    pub fn token_nl(&self) -> LlamaToken {
371        let token = unsafe { llama_cpp_bindings_sys::llama_token_nl(self.vocab_ptr()) };
372        LlamaToken(token)
373    }
374
375    #[must_use]
376    pub fn is_eog_token(&self, token: &SampledToken) -> bool {
377        let (SampledToken::Content(LlamaToken(id))
378        | SampledToken::Reasoning(LlamaToken(id))
379        | SampledToken::ToolCall(LlamaToken(id))
380        | SampledToken::Undeterminable(LlamaToken(id))) = *token;
381
382        unsafe { llama_cpp_bindings_sys::llama_token_is_eog(self.vocab_ptr(), id) }
383    }
384
385    #[must_use]
386    pub fn decode_start_token(&self) -> LlamaToken {
387        let token =
388            unsafe { llama_cpp_bindings_sys::llama_model_decoder_start_token(self.model.as_ptr()) };
389        LlamaToken(token)
390    }
391
392    #[must_use]
393    pub fn token_sep(&self) -> LlamaToken {
394        let token = unsafe { llama_cpp_bindings_sys::llama_vocab_sep(self.vocab_ptr()) };
395        LlamaToken(token)
396    }
397
398    /// # Errors
399    ///
400    /// - if [`str`] contains a null byte
401    /// - if an integer conversion fails during tokenization
402    ///
403    ///
404    /// ```no_run
405    /// use llama_cpp_bindings::model::LlamaModel;
406    ///
407    pub fn str_to_token(
408        &self,
409        str: &str,
410        add_bos: AddBos,
411    ) -> Result<Vec<LlamaToken>, StringToTokenError> {
412        let add_bos = match add_bos {
413            AddBos::Always => true,
414            AddBos::Never => false,
415        };
416
417        let tokens_estimation = std::cmp::max(8, (str.len() / 2) + usize::from(add_bos));
418        let (c_string, c_string_len) = cstring_with_validated_len(str)?;
419        let vocab = self.vocab_ptr();
420
421        tokenize_into_buffer(tokens_estimation, |tokens, n_tokens_max| {
422            invoke_rs_tokenize(
423                vocab,
424                c_string.as_ptr(),
425                c_string_len,
426                tokens,
427                n_tokens_max,
428                add_bos,
429            )
430        })
431    }
432
433    /// # Errors
434    ///
435    /// Returns an error if the token type is not known to this library.
436    pub fn token_attr(
437        &self,
438        LlamaToken(id): LlamaToken,
439    ) -> Result<LlamaTokenAttrs, LlamaTokenAttrsFromIntError> {
440        let token_type =
441            unsafe { llama_cpp_bindings_sys::llama_token_get_attr(self.vocab_ptr(), id) };
442
443        LlamaTokenAttrs::try_from(token_type)
444    }
445
446    /// # Errors
447    ///
448    /// - if the token type is unknown
449    ///
450    /// - if the returned size from llama.cpp does not fit into a `usize`
451    pub fn token_to_piece(
452        &self,
453        token: &SampledToken,
454        decoder: &mut encoding_rs::Decoder,
455        special: bool,
456        lstrip: Option<NonZeroU16>,
457    ) -> Result<String, TokenToStringError> {
458        let (SampledToken::Content(inner)
459        | SampledToken::Reasoning(inner)
460        | SampledToken::ToolCall(inner)
461        | SampledToken::Undeterminable(inner)) = *token;
462        let bytes = match self.token_to_piece_bytes(inner, 8, special, lstrip) {
463            Err(TokenToStringError::InsufficientBufferSpace(required_size)) => {
464                let buffer_size: usize = (-required_size).try_into()?;
465
466                self.token_to_piece_bytes(inner, buffer_size, special, lstrip)
467            }
468            other => other,
469        }?;
470
471        let mut output_piece = String::with_capacity(bytes.len());
472        let (_result, _decoded_size, _had_replacements) =
473            decoder.decode_to_string(&bytes, &mut output_piece, false);
474
475        Ok(output_piece)
476    }
477
478    /// # Errors
479    ///
480    /// - if the token type is unknown
481    /// - the resultant token is larger than `buffer_size`.
482    /// - if an integer conversion fails
483    pub fn token_to_piece_bytes(
484        &self,
485        token: LlamaToken,
486        buffer_size: usize,
487        special: bool,
488        lstrip: Option<NonZeroU16>,
489    ) -> Result<Vec<u8>, TokenToStringError> {
490        let mut buffer: Vec<u8> = vec![0u8; buffer_size];
491        let buffer_len = c_int::try_from(buffer.len())?;
492        let lstrip = lstrip.map_or(0, |strip_count| i32::from(strip_count.get()));
493        let size = unsafe {
494            llama_cpp_bindings_sys::llama_token_to_piece(
495                self.vocab_ptr(),
496                token.0,
497                buffer.as_mut_ptr().cast::<c_char>(),
498                buffer_len,
499                lstrip,
500                special,
501            )
502        };
503
504        match size {
505            0 => Err(TokenToStringError::UnknownTokenType),
506            error_code if error_code.is_negative() => {
507                Err(TokenToStringError::InsufficientBufferSpace(error_code))
508            }
509            size => {
510                let written = usize::try_from(size)?;
511                buffer.truncate(written);
512
513                Ok(buffer)
514            }
515        }
516    }
517
518    #[must_use]
519    pub fn n_vocab(&self) -> i32 {
520        unsafe { llama_cpp_bindings_sys::llama_n_vocab(self.vocab_ptr()) }
521    }
522
523    /// # Errors
524    ///
525    /// Returns an error if llama.cpp emits a vocab type that is not known to this library.
526    pub fn vocab_type(&self) -> Result<VocabType, VocabTypeFromIntError> {
527        let vocab_type = unsafe { llama_cpp_bindings_sys::llama_vocab_type(self.vocab_ptr()) };
528
529        VocabType::try_from(vocab_type)
530    }
531
532    #[must_use]
533    pub fn n_embd(&self) -> c_int {
534        unsafe { llama_cpp_bindings_sys::llama_n_embd(self.model.as_ptr()) }
535    }
536
537    #[must_use]
538    pub fn size(&self) -> u64 {
539        unsafe { llama_cpp_bindings_sys::llama_model_size(self.model.as_ptr()) }
540    }
541
542    #[must_use]
543    pub fn n_params(&self) -> u64 {
544        unsafe { llama_cpp_bindings_sys::llama_model_n_params(self.model.as_ptr()) }
545    }
546
547    #[must_use]
548    pub fn is_recurrent(&self) -> bool {
549        unsafe { llama_cpp_bindings_sys::llama_model_is_recurrent(self.model.as_ptr()) }
550    }
551
552    /// # Errors
553    ///
554    /// Returns an error if the layer count returned by llama.cpp does not fit into a `u32`.
555    pub fn n_layer(&self) -> Result<u32, std::num::TryFromIntError> {
556        u32::try_from(unsafe { llama_cpp_bindings_sys::llama_model_n_layer(self.model.as_ptr()) })
557    }
558
559    /// # Errors
560    ///
561    /// Returns an error if the head count returned by llama.cpp does not fit into a `u32`.
562    pub fn n_head(&self) -> Result<u32, std::num::TryFromIntError> {
563        u32::try_from(unsafe { llama_cpp_bindings_sys::llama_model_n_head(self.model.as_ptr()) })
564    }
565
566    /// # Errors
567    ///
568    /// Returns an error if the KV head count returned by llama.cpp does not fit into a `u32`.
569    pub fn n_head_kv(&self) -> Result<u32, std::num::TryFromIntError> {
570        u32::try_from(unsafe { llama_cpp_bindings_sys::llama_model_n_head_kv(self.model.as_ptr()) })
571    }
572
573    #[must_use]
574    pub fn is_hybrid(&self) -> bool {
575        unsafe { llama_cpp_bindings_sys::llama_model_is_hybrid(self.model.as_ptr()) }
576    }
577
578    /// # Errors
579    /// Returns an error if the key is not found or the value is not valid UTF-8.
580    pub fn meta_val_str(&self, key: &str) -> Result<String, MetaValError> {
581        let key_cstring = CString::new(key)?;
582        let key_ptr = key_cstring.as_ptr();
583
584        extract_meta_string(
585            |buf_ptr, buf_len| unsafe {
586                llama_cpp_bindings_sys::llama_model_meta_val_str(
587                    self.model.as_ptr(),
588                    key_ptr,
589                    buf_ptr,
590                    buf_len,
591                )
592            },
593            256,
594        )
595    }
596
597    #[must_use]
598    pub fn meta_count(&self) -> i32 {
599        unsafe { llama_cpp_bindings_sys::llama_model_meta_count(self.model.as_ptr()) }
600    }
601
602    /// # Errors
603    /// Returns an error if the index is out of range or the key is not valid UTF-8.
604    pub fn meta_key_by_index(&self, index: i32) -> Result<String, MetaValError> {
605        extract_meta_string(
606            |buf_ptr, buf_len| unsafe {
607                llama_cpp_bindings_sys::llama_model_meta_key_by_index(
608                    self.model.as_ptr(),
609                    index,
610                    buf_ptr,
611                    buf_len,
612                )
613            },
614            256,
615        )
616    }
617
618    /// # Errors
619    /// Returns an error if the index is out of range or the value is not valid UTF-8.
620    pub fn meta_val_str_by_index(&self, index: i32) -> Result<String, MetaValError> {
621        extract_meta_string(
622            |buf_ptr, buf_len| unsafe {
623                llama_cpp_bindings_sys::llama_model_meta_val_str_by_index(
624                    self.model.as_ptr(),
625                    index,
626                    buf_ptr,
627                    buf_len,
628                )
629            },
630            256,
631        )
632    }
633
634    #[must_use]
635    pub fn rope_type(&self) -> Option<RopeType> {
636        let raw = unsafe { llama_cpp_bindings_sys::llama_model_rope_type(self.model.as_ptr()) };
637
638        rope_type::rope_type_from_raw(raw)
639    }
640
641    /// # Errors
642    ///
643    /// * If the model has no chat template by that name
644    ///
645    /// # Panics
646    ///
647    /// Panics if the C-returned chat template string contains interior null bytes
648    /// (should never happen with valid model data).
649    pub fn chat_template(
650        &self,
651        name: Option<&str>,
652    ) -> Result<LlamaChatTemplate, ChatTemplateError> {
653        let name_cstr = name.map(CString::new);
654        let name_ptr = match name_cstr {
655            Some(Ok(name)) => name.as_ptr(),
656            _ => ptr::null(),
657        };
658        let result = unsafe {
659            llama_cpp_bindings_sys::llama_model_chat_template(self.model.as_ptr(), name_ptr)
660        };
661
662        if result.is_null() {
663            Err(ChatTemplateError::MissingTemplate)
664        } else {
665            let chat_template_cstr = unsafe { CStr::from_ptr(result) };
666
667            Ok(LlamaChatTemplate(chat_template_cstr.to_owned()))
668        }
669    }
670
671    /// # Errors
672    ///
673    /// See [`LlamaModelLoadError`] for more information.
674    ///
675    /// # Panics
676    ///
677    /// Panics if a valid UTF-8 path somehow contains interior null bytes (should never happen).
678    pub fn load_from_file(
679        _: &LlamaBackend,
680        path: impl AsRef<Path>,
681        params: &LlamaModelParams,
682    ) -> Result<Self, LlamaModelLoadError> {
683        let path = path.as_ref();
684
685        let path_str = path
686            .to_str()
687            .ok_or_else(|| LlamaModelLoadError::PathToStrError(path.to_path_buf()))?;
688
689        if !path.exists() {
690            return Err(LlamaModelLoadError::FileNotFound(path.to_path_buf()));
691        }
692
693        let cstr = CString::new(path_str)?;
694        let mut out_model: *mut llama_cpp_bindings_sys::llama_model = ptr::null_mut();
695        let mut out_error: *mut c_char = ptr::null_mut();
696        let status = unsafe {
697            llama_cpp_bindings_sys::llama_rs_load_model_from_file(
698                cstr.as_ptr(),
699                params.params,
700                &raw mut out_model,
701                &raw mut out_error,
702            )
703        };
704        unsafe { load_model_from_file_status_to_result(status, out_model, out_error, path) }
705    }
706
707    /// # Errors
708    ///
709    /// See [`LlamaLoraAdapterInitError`] for more information.
710    pub fn lora_adapter_init(
711        &self,
712        path: impl AsRef<Path>,
713    ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
714        let path = path.as_ref();
715
716        let path_str = path
717            .to_str()
718            .ok_or_else(|| LlamaLoraAdapterInitError::PathToStrError(path.to_path_buf()))?;
719
720        if !path.exists() {
721            return Err(LlamaLoraAdapterInitError::FileNotFound(path.to_path_buf()));
722        }
723
724        let cstr = CString::new(path_str)?;
725        let raw_adapter = unsafe {
726            llama_cpp_bindings_sys::llama_adapter_lora_init(self.model.as_ptr(), cstr.as_ptr())
727        };
728
729        let Some(adapter) = NonNull::new(raw_adapter) else {
730            return Err(LlamaLoraAdapterInitError::Unloadable);
731        };
732
733        Ok(LlamaLoraAdapter {
734            lora_adapter: adapter,
735        })
736    }
737
738    /// # Errors
739    /// Returns [`ApplyChatTemplateError`] if the model has no vocab, the template
740    /// renders an empty prompt or cannot be rendered, or the renderer throws.
741    pub fn apply_chat_template(
742        &self,
743        tmpl: &LlamaChatTemplate,
744        chat: &[LlamaChatMessage],
745        add_ass: bool,
746        enable_thinking: bool,
747    ) -> Result<String, ApplyChatTemplateError> {
748        let roles: Vec<*const c_char> = chat
749            .iter()
750            .map(|chat_message| chat_message.role.as_ptr())
751            .collect();
752        let contents: Vec<*const c_char> = chat
753            .iter()
754            .map(|chat_message| chat_message.content.as_ptr())
755            .collect();
756
757        let mut out_string: *mut c_char = ptr::null_mut();
758        let mut out_error: *mut c_char = ptr::null_mut();
759
760        let status = unsafe {
761            llama_cpp_bindings_sys::llama_rs_apply_chat_template(
762                self.model.as_ptr(),
763                tmpl.0.as_ptr(),
764                roles.as_ptr(),
765                contents.as_ptr(),
766                chat.len(),
767                i32::from(add_ass),
768                i32::from(enable_thinking),
769                &raw mut out_string,
770                &raw mut out_error,
771            )
772        };
773
774        unsafe { apply_chat_template_status_to_result(status, out_string, out_error) }
775    }
776
777    /// # Errors
778    /// Returns [`MarkerDetectionError`] when streaming-marker detection fails.
779    /// The classifier is never constructed in a degraded "blind" state — a
780    /// detection failure is surfaced to the caller instead of silently ignored.
781    pub fn sampled_token_classifier(
782        &self,
783    ) -> Result<SampledTokenClassifier<'_>, MarkerDetectionError> {
784        let markers = self.streaming_markers()?;
785
786        Ok(SampledTokenClassifier::new(self, markers))
787    }
788
789    /// # Errors
790    /// Returns [`MarkerDetectionError`] when any underlying FFI call fails.
791    pub fn streaming_markers(&self) -> Result<StreamingMarkers, MarkerDetectionError> {
792        let (reasoning_open_str, reasoning_close_str) =
793            invoke_detect_reasoning_markers(self.model.as_ptr())?;
794
795        let tool_call_haystack = invoke_compute_tool_call_haystack(self.model.as_ptr())?;
796
797        let autoparser_pair = tool_call_haystack.as_deref().and_then(
798            crate::extract_tool_call_markers_from_haystack::extract_tool_call_markers_from_haystack,
799        );
800
801        let (autoparser_open, autoparser_close) = match autoparser_pair {
802            Some(crate::tool_call_marker_pair::ToolCallMarkerPair { open, close }) => {
803                (Some(open), Some(close))
804            }
805            None => (None, None),
806        };
807
808        let resolved_tool_call_markers =
809            self.resolve_tool_call_marker_strings(autoparser_open, autoparser_close)?;
810
811        Ok(StreamingMarkers {
812            reasoning_open: self.tokenize_marker(reasoning_open_str.as_deref())?,
813            reasoning_close: self.tokenize_marker(reasoning_close_str.as_deref())?,
814            tool_call_open: self.tokenize_marker(resolved_tool_call_markers.open.as_deref())?,
815            tool_call_close: self.tokenize_marker(resolved_tool_call_markers.close.as_deref())?,
816        })
817    }
818
819    fn resolve_tool_call_marker_strings(
820        &self,
821        autoparser_open: Option<String>,
822        autoparser_close: Option<String>,
823    ) -> Result<ResolvedToolCallMarkers, MarkerDetectionError> {
824        if autoparser_open
825            .as_deref()
826            .is_some_and(|raw| !raw.trim().is_empty())
827        {
828            return Ok(ResolvedToolCallMarkers {
829                open: autoparser_open,
830                close: autoparser_close,
831            });
832        }
833        let Some(markers) = self.tool_call_markers()? else {
834            return Ok(ResolvedToolCallMarkers {
835                open: autoparser_open,
836                close: autoparser_close,
837            });
838        };
839        let close = if markers.close.is_empty() {
840            None
841        } else {
842            Some(markers.close)
843        };
844        Ok(ResolvedToolCallMarkers {
845            open: Some(markers.open),
846            close,
847        })
848    }
849
850    /// # Errors
851    /// Returns [`MarkerDetectionError`] when the underlying FFI call fails.
852    pub fn reasoning_markers(&self) -> Result<Option<ReasoningMarkers>, MarkerDetectionError> {
853        let (open, close) = invoke_detect_reasoning_markers(self.model.as_ptr())?;
854
855        if let Some(markers) = reasoning_markers_from_marker_pair(open, close) {
856            return Ok(Some(markers));
857        }
858
859        detect_reasoning_markers_via_template_probe(self.model.as_ptr())
860    }
861
862    /// # Errors
863    /// Returns [`MarkerDetectionError::ToolCallTemplateNotUtf8`] when the model
864    /// has a chat template that is not valid UTF-8. A model with no chat
865    /// template legitimately yields `Ok(None)`.
866    pub fn tool_call_markers(&self) -> Result<Option<ToolCallMarkers>, MarkerDetectionError> {
867        let template = match self.chat_template(None) {
868            Ok(template) => template,
869            Err(ChatTemplateError::MissingTemplate) => return Ok(None),
870            Err(other) => return Err(MarkerDetectionError::ChatTemplateUnavailable(other)),
871        };
872        let template_str = template.to_str()?;
873
874        Ok(tool_call_template_overrides::detect(template_str))
875    }
876
877    /// # Errors
878    /// Returns [`StringToTokenError`] when a present, non-empty marker string
879    /// fails to tokenise.
880    fn tokenize_marker(
881        &self,
882        marker: Option<&str>,
883    ) -> Result<Option<Vec<LlamaToken>>, StringToTokenError> {
884        let Some(marker) = marker else {
885            return Ok(None);
886        };
887        let marker = marker.trim();
888        if marker.is_empty() {
889            return Ok(None);
890        }
891        let tokens = self.str_to_token(marker, AddBos::Never)?;
892        if tokens.is_empty() {
893            Ok(None)
894        } else {
895            Ok(Some(tokens))
896        }
897    }
898
899    /// # Errors
900    ///
901    /// Returns [`ParseChatMessageError`] when `tools_json` is not valid JSON,
902    /// the FFI returns a non-OK status other than `ParseException`, or
903    /// accessor strings are not valid UTF-8.
904    pub fn parse_chat_message(
905        &self,
906        tools_json: &str,
907        input: &str,
908        is_partial: bool,
909    ) -> Result<ChatMessageParseOutcome, ParseChatMessageError> {
910        let tools_value: serde_json::Value =
911            serde_json::from_str(tools_json).map_err(ParseChatMessageError::ToolsJsonInvalid)?;
912        if !tools_value.is_array() {
913            return Err(ParseChatMessageError::ToolsJsonNotArray);
914        }
915
916        let reasoning_markers = self.reasoning_markers()?;
917
918        for candidate in tool_call_template_overrides::known_marker_candidates() {
919            if let ToolCallFormatOutcome::Parsed(calls) =
920                tool_call_format::try_parse(input, &candidate)
921            {
922                let split =
923                    split_reasoning_prefix(input, reasoning_markers.as_ref(), &candidate.open);
924                let mut parsed = ParsedChatMessage::new(split.content, split.reasoning, calls);
925                synthesize_missing_tool_call_ids(&mut parsed.tool_calls);
926                return Ok(ChatMessageParseOutcome::Recognized(parsed));
927            }
928        }
929
930        let via_ffi_result = self.parse_chat_message_via_ffi(tools_json, input, is_partial);
931
932        outcome_from_via_ffi_result(via_ffi_result, tools_json, input, is_partial)
933    }
934
935    fn parse_chat_message_via_ffi(
936        &self,
937        tools_json: &str,
938        input: &str,
939        is_partial: bool,
940    ) -> Result<ParsedChatMessage, ParseChatMessageError> {
941        let parser = self.chat_parser()?;
942
943        let tools_cstring = CString::new(tools_json)
944            .map_err(|err| ParseChatMessageError::ToolsSerialization(err.to_string()))?;
945        let input_cstring = CString::new(input)
946            .map_err(|err| ParseChatMessageError::ToolsSerialization(err.to_string()))?;
947
948        let mut handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat = ptr::null_mut();
949        let mut out_error: *mut c_char = ptr::null_mut();
950
951        let status = unsafe {
952            llama_cpp_bindings_sys::llama_rs_parse_chat_message(
953                parser.parser.as_ptr(),
954                tools_cstring.as_ptr(),
955                input_cstring.as_ptr(),
956                i32::from(is_partial),
957                &raw mut handle,
958                &raw mut out_error,
959            )
960        };
961
962        let parsed =
963            unsafe { parse_chat_message_status_to_result(status, handle, &raw mut out_error) };
964
965        let mut free_error: *mut c_char = ptr::null_mut();
966        let free_status = unsafe {
967            llama_cpp_bindings_sys::llama_rs_parsed_chat_free(handle, &raw mut free_error)
968        };
969        unsafe { parsed_chat_free_status_to_result(parsed, free_status, out_error, free_error) }
970    }
971
972    fn chat_parser(&self) -> Result<&ChatParserHandle, ParseChatMessageError> {
973        if let Some(parser) = self.chat_parser.get() {
974            return Ok(parser);
975        }
976        let parser = self.create_chat_parser()?;
977        Ok(self.chat_parser.get_or_init(|| parser))
978    }
979
980    fn create_chat_parser(&self) -> Result<ChatParserHandle, ParseChatMessageError> {
981        let probe_markers = detect_reasoning_markers_via_template_probe(self.model.as_ptr())?;
982
983        // SAFETY: reasoning markers are template render text and never contain an
984        // interior NUL byte, so the unchecked CString construction is sound.
985        let reasoning_open = probe_markers.as_ref().map(|markers| unsafe {
986            CString::from_vec_unchecked(markers.open.as_bytes().to_vec())
987        });
988        let reasoning_close = probe_markers.as_ref().map(|markers| unsafe {
989            CString::from_vec_unchecked(markers.close.as_bytes().to_vec())
990        });
991        let reasoning_open_ptr = reasoning_open
992            .as_ref()
993            .map_or(ptr::null(), |value| value.as_ptr());
994        let reasoning_close_ptr = reasoning_close
995            .as_ref()
996            .map_or(ptr::null(), |value| value.as_ptr());
997
998        let mut out_parser: *mut llama_cpp_bindings_sys::llama_rs_chat_parser = ptr::null_mut();
999        let mut out_error: *mut c_char = ptr::null_mut();
1000
1001        let status = unsafe {
1002            llama_cpp_bindings_sys::llama_rs_chat_parser_create(
1003                self.model.as_ptr(),
1004                reasoning_open_ptr,
1005                reasoning_close_ptr,
1006                &raw mut out_parser,
1007                &raw mut out_error,
1008            )
1009        };
1010
1011        unsafe { chat_parser_create_status_to_result(status, out_parser, &raw mut out_error) }
1012    }
1013
1014    /// # Errors
1015    ///
1016    /// Returns [`MarkerDetectionError`] when the C++ analyzer throws or the FFI
1017    /// returns a non-OK status.
1018    pub fn diagnose_tool_call_synthetic_renders(
1019        &self,
1020    ) -> Result<(String, String), MarkerDetectionError> {
1021        let (no_tools, with_tools) =
1022            invoke_diagnose_tool_call_synthetic_renders(self.model.as_ptr())?;
1023
1024        Ok((no_tools.unwrap_or_default(), with_tools.unwrap_or_default()))
1025    }
1026}
1027
1028impl LlamaModel {
1029    /// # Errors
1030    /// Returns [`TokenToStringError`] when a token's byte piece cannot be
1031    /// retrieved. The legitimate "this token has no byte piece" case is treated
1032    /// as empty (not an error); a piece that overflows the probe buffer is
1033    /// re-read at the exact size rather than dropped.
1034    pub fn approximate_tok_env(&self) -> Result<Arc<ApproximateTokEnv>, TokenToStringError> {
1035        if let Some(env) = self.tok_env.get() {
1036            return Ok(Arc::clone(env));
1037        }
1038        let env = build_approximate_tok_env(self)?;
1039        Ok(Arc::clone(self.tok_env.get_or_init(|| env)))
1040    }
1041}
1042
1043const TOK_ENV_PIECE_PROBE_SIZE: usize = 32;
1044
1045fn token_piece_bytes_for_tok_env(
1046    model: &LlamaModel,
1047    token: LlamaToken,
1048    special: bool,
1049) -> Result<Vec<u8>, TokenToStringError> {
1050    match model.token_to_piece_bytes(token, TOK_ENV_PIECE_PROBE_SIZE, special, None) {
1051        Ok(bytes) => Ok(bytes),
1052        Err(TokenToStringError::UnknownTokenType) => Ok(Vec::new()),
1053        Err(TokenToStringError::InsufficientBufferSpace(required)) => {
1054            model.token_to_piece_bytes(token, required.unsigned_abs() as usize, special, None)
1055        }
1056        Err(other) => Err(other),
1057    }
1058}
1059
1060fn build_approximate_tok_env(
1061    model: &LlamaModel,
1062) -> Result<Arc<ApproximateTokEnv>, TokenToStringError> {
1063    let n_vocab = model.n_vocab().cast_unsigned();
1064    let tok_eos = {
1065        let eot = unsafe { llama_cpp_bindings_sys::llama_vocab_eot(model.vocab_ptr()) };
1066        if eot == -1 {
1067            model.token_eos().0.cast_unsigned()
1068        } else {
1069            eot.cast_unsigned()
1070        }
1071    };
1072    let info = TokRxInfo::new(n_vocab, tok_eos);
1073
1074    let mut words = Vec::with_capacity(n_vocab as usize);
1075
1076    for token_id in 0..n_vocab.cast_signed() {
1077        let token = LlamaToken(token_id);
1078        let bytes = token_piece_bytes_for_tok_env(model, token, false)?;
1079        if bytes.is_empty() {
1080            let special_bytes = token_piece_bytes_for_tok_env(model, token, true)?;
1081            if special_bytes.is_empty() {
1082                words.push(vec![]);
1083            } else {
1084                let mut marked = Vec::with_capacity(special_bytes.len() + 1);
1085                marked.push(0xFF);
1086                marked.extend(special_bytes);
1087                words.push(marked);
1088            }
1089        } else {
1090            words.push(bytes);
1091        }
1092    }
1093
1094    let trie = TokTrie::from(&info, &words);
1095    Ok(Arc::new(ApproximateTokEnv::new(trie)))
1096}
1097
1098fn collect_parsed_chat_message(
1099    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1100) -> Result<ParsedChatMessage, ParseChatMessageError> {
1101    if handle.is_null() {
1102        return Ok(ParsedChatMessage::default());
1103    }
1104
1105    let content = read_parsed_chat_content(handle)?;
1106    let reasoning_content = read_parsed_chat_reasoning_content(handle)?;
1107    let count = read_parsed_chat_tool_call_count(handle)?;
1108
1109    let mut tool_calls = Vec::with_capacity(count);
1110    for index in 0..count {
1111        let id = read_parsed_chat_tool_call_id(handle, index)?;
1112        let name = read_parsed_chat_tool_call_name(handle, index)?;
1113        let arguments_json = read_parsed_chat_tool_call_arguments(handle, index)?;
1114
1115        let arguments = ToolCallArguments::from_string(arguments_json);
1116        tool_calls.push(ParsedToolCall::new(id, name, arguments));
1117    }
1118
1119    Ok(ParsedChatMessage::new(
1120        content,
1121        reasoning_content,
1122        tool_calls,
1123    ))
1124}
1125
1126// SAFETY: `out_string` and `out_error` must be the pointers populated by the
1127// preceding `llama_rs_parsed_chat_content` call (or null when no value/error
1128// was produced); each is read and freed in exactly one match arm.
1129unsafe fn parsed_chat_content_status_to_result(
1130    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_content_status,
1131    out_string: *mut c_char,
1132    out_error: *mut c_char,
1133) -> Result<String, ParseChatMessageError> {
1134    match status {
1135        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_OK => {
1136            consume_accessor_string(out_string)
1137        }
1138        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_ERROR_STRING_ALLOCATION_FAILED => {
1139            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1140            Err(ParseChatMessageError::NotEnoughMemory)
1141        }
1142        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_VENDORED_THREW_CXX_EXCEPTION => {
1143            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1144            Err(ParseChatMessageError::Reported { message })
1145        }
1146        other => unreachable!("llama_rs_parsed_chat_content returned unrecognized status {other}"),
1147    }
1148}
1149
1150fn read_parsed_chat_content(
1151    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1152) -> Result<String, ParseChatMessageError> {
1153    let mut out_string: *mut c_char = ptr::null_mut();
1154    let mut out_error: *mut c_char = ptr::null_mut();
1155    let status = unsafe {
1156        llama_cpp_bindings_sys::llama_rs_parsed_chat_content(
1157            handle,
1158            &raw mut out_string,
1159            &raw mut out_error,
1160        )
1161    };
1162    unsafe { parsed_chat_content_status_to_result(status, out_string, out_error) }
1163}
1164
1165// SAFETY: `out_string` and `out_error` must be the pointers populated by the
1166// preceding `llama_rs_parsed_chat_reasoning_content` call (or null when no
1167// value/error was produced); each is read and freed in exactly one match arm.
1168unsafe fn parsed_chat_reasoning_content_status_to_result(
1169    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_reasoning_content_status,
1170    out_string: *mut c_char,
1171    out_error: *mut c_char,
1172) -> Result<String, ParseChatMessageError> {
1173    match status {
1174        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_OK => {
1175            consume_accessor_string(out_string)
1176        }
1177        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_ERROR_STRING_ALLOCATION_FAILED => {
1178            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1179            Err(ParseChatMessageError::NotEnoughMemory)
1180        }
1181        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_VENDORED_THREW_CXX_EXCEPTION => {
1182            let message =
1183                unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1184            Err(ParseChatMessageError::Reported { message })
1185        }
1186        other => unreachable!(
1187            "llama_rs_parsed_chat_reasoning_content returned unrecognized status {other}"
1188        ),
1189    }
1190}
1191
1192fn read_parsed_chat_reasoning_content(
1193    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1194) -> Result<String, ParseChatMessageError> {
1195    let mut out_string: *mut c_char = ptr::null_mut();
1196    let mut out_error: *mut c_char = ptr::null_mut();
1197    let status = unsafe {
1198        llama_cpp_bindings_sys::llama_rs_parsed_chat_reasoning_content(
1199            handle,
1200            &raw mut out_string,
1201            &raw mut out_error,
1202        )
1203    };
1204    unsafe { parsed_chat_reasoning_content_status_to_result(status, out_string, out_error) }
1205}
1206
1207// SAFETY: `out_error` must be the pointer populated by the preceding
1208// `llama_rs_parsed_chat_tool_call_count` call (or null when no error was
1209// produced); it is freed in exactly one match arm.
1210unsafe fn parsed_chat_tool_call_count_status_to_result(
1211    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_count_status,
1212    out_count: usize,
1213    out_error: *mut c_char,
1214) -> Result<usize, ParseChatMessageError> {
1215    match status {
1216        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_OK => Ok(out_count),
1217        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_ERROR_STRING_ALLOCATION_FAILED => {
1218            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1219            Err(ParseChatMessageError::NotEnoughMemory)
1220        }
1221        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_VENDORED_THREW_CXX_EXCEPTION => {
1222            let message =
1223                unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1224            Err(ParseChatMessageError::Reported { message })
1225        }
1226        other => unreachable!(
1227            "llama_rs_parsed_chat_tool_call_count returned unrecognized status {other}"
1228        ),
1229    }
1230}
1231
1232fn read_parsed_chat_tool_call_count(
1233    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1234) -> Result<usize, ParseChatMessageError> {
1235    let mut out_count: usize = 0;
1236    let mut out_error: *mut c_char = ptr::null_mut();
1237    let status = unsafe {
1238        llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_count(
1239            handle,
1240            &raw mut out_count,
1241            &raw mut out_error,
1242        )
1243    };
1244    unsafe { parsed_chat_tool_call_count_status_to_result(status, out_count, out_error) }
1245}
1246
1247// SAFETY: `out_string` and `out_error` must be the pointers populated by the
1248// preceding `llama_rs_parsed_chat_tool_call_id` call (or null when no
1249// value/error was produced); each is read and freed in exactly one match arm.
1250unsafe fn parsed_chat_tool_call_id_status_to_result(
1251    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_id_status,
1252    index: usize,
1253    out_string: *mut c_char,
1254    out_error: *mut c_char,
1255) -> Result<String, ParseChatMessageError> {
1256    match status {
1257        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_OK => {
1258            consume_accessor_string(out_string)
1259        }
1260        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_INDEX_OUT_OF_BOUNDS => {
1261            Err(ParseChatMessageError::ToolCallIdIndexOutOfBounds { index })
1262        }
1263        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_ERROR_STRING_ALLOCATION_FAILED => {
1264            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1265            Err(ParseChatMessageError::NotEnoughMemory)
1266        }
1267        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_VENDORED_THREW_CXX_EXCEPTION => {
1268            let message =
1269                unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1270            Err(ParseChatMessageError::Reported { message })
1271        }
1272        other => unreachable!(
1273            "llama_rs_parsed_chat_tool_call_id returned unrecognized status {other}"
1274        ),
1275    }
1276}
1277
1278fn read_parsed_chat_tool_call_id(
1279    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1280    index: usize,
1281) -> Result<String, ParseChatMessageError> {
1282    let mut out_string: *mut c_char = ptr::null_mut();
1283    let mut out_error: *mut c_char = ptr::null_mut();
1284    let status = unsafe {
1285        llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_id(
1286            handle,
1287            index,
1288            &raw mut out_string,
1289            &raw mut out_error,
1290        )
1291    };
1292    unsafe { parsed_chat_tool_call_id_status_to_result(status, index, out_string, out_error) }
1293}
1294
1295// SAFETY: `out_string` and `out_error` must be the pointers populated by the
1296// preceding `llama_rs_parsed_chat_tool_call_name` call (or null when no
1297// value/error was produced); each is read and freed in exactly one match arm.
1298unsafe fn parsed_chat_tool_call_name_status_to_result(
1299    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_name_status,
1300    index: usize,
1301    out_string: *mut c_char,
1302    out_error: *mut c_char,
1303) -> Result<String, ParseChatMessageError> {
1304    match status {
1305        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_OK => {
1306            consume_accessor_string(out_string)
1307        }
1308        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_INDEX_OUT_OF_BOUNDS => {
1309            Err(ParseChatMessageError::ToolCallNameIndexOutOfBounds { index })
1310        }
1311        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_ERROR_STRING_ALLOCATION_FAILED => {
1312            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1313            Err(ParseChatMessageError::NotEnoughMemory)
1314        }
1315        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_VENDORED_THREW_CXX_EXCEPTION => {
1316            let message =
1317                unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1318            Err(ParseChatMessageError::Reported { message })
1319        }
1320        other => unreachable!(
1321            "llama_rs_parsed_chat_tool_call_name returned unrecognized status {other}"
1322        ),
1323    }
1324}
1325
1326fn read_parsed_chat_tool_call_name(
1327    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1328    index: usize,
1329) -> Result<String, ParseChatMessageError> {
1330    let mut out_string: *mut c_char = ptr::null_mut();
1331    let mut out_error: *mut c_char = ptr::null_mut();
1332    let status = unsafe {
1333        llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_name(
1334            handle,
1335            index,
1336            &raw mut out_string,
1337            &raw mut out_error,
1338        )
1339    };
1340    unsafe { parsed_chat_tool_call_name_status_to_result(status, index, out_string, out_error) }
1341}
1342
1343// SAFETY: `out_string` and `out_error` must be the pointers populated by the
1344// preceding `llama_rs_parsed_chat_tool_call_arguments` call (or null when no
1345// value/error was produced); each is read and freed in exactly one match arm.
1346unsafe fn parsed_chat_tool_call_arguments_status_to_result(
1347    status: llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_arguments_status,
1348    index: usize,
1349    out_string: *mut c_char,
1350    out_error: *mut c_char,
1351) -> Result<String, ParseChatMessageError> {
1352    match status {
1353        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_OK => {
1354            consume_accessor_string(out_string)
1355        }
1356        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_INDEX_OUT_OF_BOUNDS => {
1357            Err(ParseChatMessageError::ToolCallArgumentsIndexOutOfBounds { index })
1358        }
1359        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_ERROR_STRING_ALLOCATION_FAILED => {
1360            unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1361            Err(ParseChatMessageError::NotEnoughMemory)
1362        }
1363        llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_VENDORED_THREW_CXX_EXCEPTION => {
1364            let message =
1365                unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1366            Err(ParseChatMessageError::Reported { message })
1367        }
1368        other => unreachable!(
1369            "llama_rs_parsed_chat_tool_call_arguments returned unrecognized status {other}"
1370        ),
1371    }
1372}
1373
1374fn read_parsed_chat_tool_call_arguments(
1375    handle: *mut llama_cpp_bindings_sys::llama_rs_parsed_chat,
1376    index: usize,
1377) -> Result<String, ParseChatMessageError> {
1378    let mut out_string: *mut c_char = ptr::null_mut();
1379    let mut out_error: *mut c_char = ptr::null_mut();
1380    let status = unsafe {
1381        llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_arguments(
1382            handle,
1383            index,
1384            &raw mut out_string,
1385            &raw mut out_error,
1386        )
1387    };
1388    unsafe {
1389        parsed_chat_tool_call_arguments_status_to_result(status, index, out_string, out_error)
1390    }
1391}
1392
1393fn consume_accessor_string(ptr: *mut c_char) -> Result<String, ParseChatMessageError> {
1394    if ptr.is_null() {
1395        return Ok(String::new());
1396    }
1397    let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes().to_vec();
1398    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(ptr) };
1399    Ok(String::from_utf8(bytes)?)
1400}
1401
1402struct ReasoningSplit {
1403    reasoning: String,
1404    content: String,
1405}
1406
1407fn split_reasoning_prefix(
1408    input: &str,
1409    reasoning_markers: Option<&ReasoningMarkers>,
1410    tool_call_open: &str,
1411) -> ReasoningSplit {
1412    let content_only = || ReasoningSplit {
1413        reasoning: String::new(),
1414        content: prefix_before(input, tool_call_open),
1415    };
1416
1417    let Some(reasoning_markers) = reasoning_markers else {
1418        return content_only();
1419    };
1420    let Some(open_pos) = input.find(&reasoning_markers.open) else {
1421        return content_only();
1422    };
1423
1424    let after_open = &input[open_pos + reasoning_markers.open.len()..];
1425    let Some(close_offset) = after_open.find(&reasoning_markers.close) else {
1426        return content_only();
1427    };
1428
1429    let reasoning = after_open[..close_offset].to_owned();
1430    let after_close = &after_open[close_offset + reasoning_markers.close.len()..];
1431
1432    ReasoningSplit {
1433        reasoning,
1434        content: prefix_before(after_close, tool_call_open),
1435    }
1436}
1437
1438fn prefix_before(text: &str, marker: &str) -> String {
1439    text.find(marker)
1440        .map_or_else(|| text.to_owned(), |pos| text[..pos].to_owned())
1441}
1442
1443fn synthesize_missing_tool_call_ids(tool_calls: &mut [ParsedToolCall]) {
1444    for (index, call) in tool_calls.iter_mut().enumerate() {
1445        if call.id.is_empty() {
1446            call.id = format!("call_{index}");
1447        }
1448    }
1449}
1450
1451// SAFETY: `out_open`, `out_close`, and `out_error` must be the pointers
1452// populated by the preceding `llama_rs_detect_reasoning_markers` call (or null).
1453// `out_open`/`out_close` are read but not freed here; `out_error` is freed only
1454// in the CXX-exception arm, mirroring the conditional cleanup in the caller.
1455unsafe fn detect_reasoning_markers_status_to_result(
1456    status: llama_cpp_bindings_sys::llama_rs_detect_reasoning_markers_status,
1457    out_open: *const c_char,
1458    out_close: *const c_char,
1459    out_error: *mut c_char,
1460) -> Result<(Option<String>, Option<String>), MarkerDetectionError> {
1461    match status {
1462        llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_OK => {
1463            collect_optional_cstr_pair(out_open, out_close)
1464        }
1465        llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_ERROR_STRING_ALLOCATION_FAILED => {
1466            Err(MarkerDetectionError::NotEnoughMemory)
1467        }
1468        llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_VENDORED_THREW_CXX_EXCEPTION => {
1469            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1470            Err(MarkerDetectionError::ReasoningMarkerDetectionFailed { message })
1471        }
1472        other => unreachable!(
1473            "llama_rs_detect_reasoning_markers returned unrecognized status {other}"
1474        ),
1475    }
1476}
1477
1478const fn cxx_exception_owns_out_error<TValue>(
1479    parsed: &Result<TValue, MarkerDetectionError>,
1480) -> bool {
1481    matches!(
1482        parsed,
1483        Err(MarkerDetectionError::ReasoningMarkerDetectionFailed { .. }
1484            | MarkerDetectionError::ToolCallHaystackComputationFailed { .. }
1485            | MarkerDetectionError::ToolCallSyntheticRenderDiagnosisFailed { .. })
1486    )
1487}
1488
1489fn invoke_detect_reasoning_markers(
1490    model: *const llama_cpp_bindings_sys::llama_model,
1491) -> Result<(Option<String>, Option<String>), MarkerDetectionError> {
1492    let mut out_open: *mut c_char = ptr::null_mut();
1493    let mut out_close: *mut c_char = ptr::null_mut();
1494    let mut out_error: *mut c_char = ptr::null_mut();
1495
1496    let status = unsafe {
1497        llama_cpp_bindings_sys::llama_rs_detect_reasoning_markers(
1498            model,
1499            &raw mut out_open,
1500            &raw mut out_close,
1501            &raw mut out_error,
1502        )
1503    };
1504
1505    let parsed = unsafe {
1506        detect_reasoning_markers_status_to_result(status, out_open, out_close, out_error)
1507    };
1508
1509    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_open) };
1510    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_close) };
1511    if !cxx_exception_owns_out_error(&parsed) {
1512        unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1513    }
1514
1515    parsed
1516}
1517
1518// SAFETY: `out_rendered` and `out_error` must be the pointers populated by the
1519// preceding `llama_rs_render_chat_template` call (or null). `out_rendered` is
1520// read but not freed here; `out_error` is freed only in the CXX-exception arm,
1521// mirroring the conditional cleanup in the caller.
1522unsafe fn render_chat_template_status_to_result(
1523    status: llama_cpp_bindings_sys::llama_rs_render_chat_template_status,
1524    out_rendered: *const c_char,
1525    out_error: *mut c_char,
1526) -> Result<Option<String>, MarkerDetectionError> {
1527    match status {
1528        llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_OK => {
1529            read_optional_owned_cstr(out_rendered)
1530        }
1531        llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_MODEL_HAS_NO_CHAT_TEMPLATE
1532        | llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_MODEL_HAS_NO_VOCAB => Ok(None),
1533        llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_ERROR_STRING_ALLOCATION_FAILED => {
1534            Err(MarkerDetectionError::NotEnoughMemory)
1535        }
1536        llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_VENDORED_THREW_CXX_EXCEPTION => {
1537            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1538            Err(MarkerDetectionError::ReasoningMarkerDetectionFailed { message })
1539        }
1540        other => {
1541            unreachable!("llama_rs_render_chat_template returned unrecognized status {other}")
1542        }
1543    }
1544}
1545
1546fn render_chat_template(
1547    model: *const llama_cpp_bindings_sys::llama_model,
1548    messages_json: &str,
1549) -> Result<Option<String>, MarkerDetectionError> {
1550    // SAFETY: `messages_json` is serde_json output, which never emits an interior
1551    // NUL byte, so the unchecked CString construction has no NUL to trip over.
1552    let messages = unsafe { CString::from_vec_unchecked(messages_json.as_bytes().to_vec()) };
1553    let mut out_rendered: *mut c_char = ptr::null_mut();
1554    let mut out_error: *mut c_char = ptr::null_mut();
1555
1556    let status = unsafe {
1557        llama_cpp_bindings_sys::llama_rs_render_chat_template(
1558            model,
1559            messages.as_ptr(),
1560            0,
1561            1,
1562            &raw mut out_rendered,
1563            &raw mut out_error,
1564        )
1565    };
1566
1567    let parsed = unsafe { render_chat_template_status_to_result(status, out_rendered, out_error) };
1568
1569    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_rendered) };
1570    if !cxx_exception_owns_out_error(&parsed) {
1571        unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1572    }
1573
1574    parsed
1575}
1576
1577// The reasoning-marker probe is best-effort. A template that cannot render the
1578// probe's structured-content messages (e.g. a Jinja template expecting string
1579// content throws "unexpected item type in content") simply makes the probe
1580// inapplicable, yielding no markers — mirroring the original C++ probe's
1581// catch-and-continue. Genuine resource failures still propagate.
1582fn render_probe_messages(
1583    model: *const llama_cpp_bindings_sys::llama_model,
1584    messages_json: &str,
1585) -> Result<Option<String>, MarkerDetectionError> {
1586    match render_chat_template(model, messages_json) {
1587        Ok(rendered) => Ok(rendered),
1588        Err(MarkerDetectionError::ReasoningMarkerDetectionFailed { .. }) => Ok(None),
1589        Err(other) => Err(other),
1590    }
1591}
1592
1593fn detect_reasoning_markers_via_template_probe(
1594    model: *const llama_cpp_bindings_sys::llama_model,
1595) -> Result<Option<ReasoningMarkers>, MarkerDetectionError> {
1596    use crate::extract_reasoning_markers_from_probe_renders::chunked_probe_messages_json;
1597    use crate::extract_reasoning_markers_from_probe_renders::extract_reasoning_markers_from_probe_renders;
1598    use crate::extract_reasoning_markers_from_probe_renders::plain_probe_messages_json;
1599
1600    let Some(plain_render) = render_probe_messages(model, &plain_probe_messages_json())? else {
1601        return Ok(None);
1602    };
1603    let Some(chunked_render) = render_probe_messages(model, &chunked_probe_messages_json())? else {
1604        return Ok(None);
1605    };
1606
1607    Ok(extract_reasoning_markers_from_probe_renders(
1608        &plain_render,
1609        &chunked_render,
1610    ))
1611}
1612
1613// SAFETY: `out_haystack` and `out_error` must be the pointers populated by the
1614// preceding `llama_rs_compute_tool_call_haystack` call (or null). `out_haystack`
1615// is read but not freed here; `out_error` is freed only in the CXX-exception
1616// arm, mirroring the conditional cleanup in the caller.
1617unsafe fn compute_tool_call_haystack_status_to_result(
1618    status: llama_cpp_bindings_sys::llama_rs_compute_tool_call_haystack_status,
1619    out_haystack: *const c_char,
1620    out_error: *mut c_char,
1621) -> Result<Option<String>, MarkerDetectionError> {
1622    match status {
1623        llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_OK => {
1624            read_optional_owned_cstr(out_haystack)
1625        }
1626        llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_ERROR_STRING_ALLOCATION_FAILED => {
1627            Err(MarkerDetectionError::NotEnoughMemory)
1628        }
1629        llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_VENDORED_THREW_CXX_EXCEPTION => {
1630            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1631            Err(MarkerDetectionError::ToolCallHaystackComputationFailed { message })
1632        }
1633        other => unreachable!(
1634            "llama_rs_compute_tool_call_haystack returned unrecognized status {other}"
1635        ),
1636    }
1637}
1638
1639fn invoke_compute_tool_call_haystack(
1640    model: *const llama_cpp_bindings_sys::llama_model,
1641) -> Result<Option<String>, MarkerDetectionError> {
1642    let mut out_haystack: *mut c_char = ptr::null_mut();
1643    let mut out_error: *mut c_char = ptr::null_mut();
1644
1645    let status = unsafe {
1646        llama_cpp_bindings_sys::llama_rs_compute_tool_call_haystack(
1647            model,
1648            &raw mut out_haystack,
1649            &raw mut out_error,
1650        )
1651    };
1652
1653    let parsed =
1654        unsafe { compute_tool_call_haystack_status_to_result(status, out_haystack, out_error) };
1655
1656    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_haystack) };
1657    if !cxx_exception_owns_out_error(&parsed) {
1658        unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1659    }
1660
1661    parsed
1662}
1663
1664// SAFETY: `out_no_tools`, `out_with_tools`, and `out_error` must be the pointers
1665// populated by the preceding `llama_rs_diagnose_tool_call_synthetic_renders`
1666// call (or null). The render pointers are read but not freed here; `out_error`
1667// is freed only in the CXX-exception arm, mirroring the cleanup in the caller.
1668unsafe fn diagnose_tool_call_synthetic_renders_status_to_result(
1669    status: llama_cpp_bindings_sys::llama_rs_diagnose_tool_call_synthetic_renders_status,
1670    out_no_tools: *const c_char,
1671    out_with_tools: *const c_char,
1672    out_error: *mut c_char,
1673) -> Result<(Option<String>, Option<String>), MarkerDetectionError> {
1674    match status {
1675        llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_OK => {
1676            collect_optional_cstr_pair(out_no_tools, out_with_tools)
1677        }
1678        llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_ERROR_STRING_ALLOCATION_FAILED => {
1679            Err(MarkerDetectionError::NotEnoughMemory)
1680        }
1681        llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_VENDORED_THREW_CXX_EXCEPTION => {
1682            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1683            Err(MarkerDetectionError::ToolCallSyntheticRenderDiagnosisFailed { message })
1684        }
1685        other => unreachable!(
1686            "llama_rs_diagnose_tool_call_synthetic_renders returned unrecognized status {other}"
1687        ),
1688    }
1689}
1690
1691fn invoke_diagnose_tool_call_synthetic_renders(
1692    model: *const llama_cpp_bindings_sys::llama_model,
1693) -> Result<(Option<String>, Option<String>), MarkerDetectionError> {
1694    let mut out_no_tools: *mut c_char = ptr::null_mut();
1695    let mut out_with_tools: *mut c_char = ptr::null_mut();
1696    let mut out_error: *mut c_char = ptr::null_mut();
1697
1698    let status = unsafe {
1699        llama_cpp_bindings_sys::llama_rs_diagnose_tool_call_synthetic_renders(
1700            model,
1701            &raw mut out_no_tools,
1702            &raw mut out_with_tools,
1703            &raw mut out_error,
1704        )
1705    };
1706
1707    let parsed = unsafe {
1708        diagnose_tool_call_synthetic_renders_status_to_result(
1709            status,
1710            out_no_tools,
1711            out_with_tools,
1712            out_error,
1713        )
1714    };
1715
1716    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_no_tools) };
1717    unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_with_tools) };
1718    if !cxx_exception_owns_out_error(&parsed) {
1719        unsafe { llama_cpp_bindings_sys::llama_rs_string_free(out_error) };
1720    }
1721
1722    parsed
1723}
1724
1725fn read_optional_owned_cstr(ptr: *const c_char) -> Result<Option<String>, MarkerDetectionError> {
1726    if ptr.is_null() {
1727        return Ok(None);
1728    }
1729
1730    let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes().to_vec();
1731
1732    Ok(Some(String::from_utf8(bytes)?))
1733}
1734
1735// SAFETY: `out_error` must be the pointer populated by the preceding
1736// `llama_rs_tokenize` call (or null when no error was produced); it is read and
1737// freed only in the CXX-exception arm.
1738unsafe fn tokenize_status_to_result(
1739    status: llama_cpp_bindings_sys::llama_rs_tokenize_status,
1740    out_count: c_int,
1741    out_error: *mut c_char,
1742) -> Result<c_int, StringToTokenError> {
1743    match status {
1744        llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_OK => Ok(out_count),
1745        llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_ERROR_STRING_ALLOCATION_FAILED => {
1746            Err(StringToTokenError::NotEnoughMemory)
1747        }
1748        llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_VENDORED_THREW_CXX_EXCEPTION => {
1749            let message = unsafe { crate::ffi_error_reader::read_and_free_cpp_error(out_error) };
1750            Err(StringToTokenError::Reported { message })
1751        }
1752        other => unreachable!("llama_rs_tokenize returned unrecognized status {other}"),
1753    }
1754}
1755
1756fn invoke_rs_tokenize(
1757    vocab: *const llama_cpp_bindings_sys::llama_vocab,
1758    text: *const c_char,
1759    text_len: c_int,
1760    tokens: *mut llama_cpp_bindings_sys::llama_token,
1761    n_tokens_max: c_int,
1762    add_bos: bool,
1763) -> Result<c_int, StringToTokenError> {
1764    let mut out_count: i32 = 0;
1765    let mut out_error: *mut c_char = ptr::null_mut();
1766    let status = unsafe {
1767        llama_cpp_bindings_sys::llama_rs_tokenize(
1768            vocab,
1769            text,
1770            text_len,
1771            tokens,
1772            n_tokens_max,
1773            add_bos,
1774            true,
1775            &raw mut out_count,
1776            &raw mut out_error,
1777        )
1778    };
1779    unsafe { tokenize_status_to_result(status, out_count, out_error) }
1780}
1781
1782fn checked_token_buffer_capacity(capacity: usize) -> Result<c_int, StringToTokenError> {
1783    Ok(c_int::try_from(capacity)?)
1784}
1785
1786fn checked_token_count(size: i32) -> Result<usize, StringToTokenError> {
1787    Ok(usize::try_from(size)?)
1788}
1789
1790fn tokenize_into_buffer(
1791    estimated_capacity: usize,
1792    invoke: impl Fn(
1793        *mut llama_cpp_bindings_sys::llama_token,
1794        c_int,
1795    ) -> Result<c_int, StringToTokenError>,
1796) -> Result<Vec<LlamaToken>, StringToTokenError> {
1797    let mut buffer: Vec<LlamaToken> = Vec::with_capacity(estimated_capacity);
1798    let buffer_capacity = checked_token_buffer_capacity(buffer.capacity())?;
1799
1800    let size = invoke(
1801        buffer
1802            .as_mut_ptr()
1803            .cast::<llama_cpp_bindings_sys::llama_token>(),
1804        buffer_capacity,
1805    )?;
1806
1807    let size = if size.is_negative() {
1808        buffer.reserve_exact(checked_token_count(-size)?);
1809        invoke(
1810            buffer
1811                .as_mut_ptr()
1812                .cast::<llama_cpp_bindings_sys::llama_token>(),
1813            -size,
1814        )?
1815    } else {
1816        size
1817    };
1818
1819    let size = checked_token_count(size)?;
1820
1821    // SAFETY: `size` <= `capacity` and llama-cpp has initialized elements up to `size`
1822    unsafe { buffer.set_len(size) }
1823
1824    Ok(buffer)
1825}
1826
1827fn collect_optional_cstr_pair(
1828    first_ptr: *const c_char,
1829    second_ptr: *const c_char,
1830) -> Result<(Option<String>, Option<String>), MarkerDetectionError> {
1831    let first = read_optional_owned_cstr(first_ptr)?;
1832    let second = read_optional_owned_cstr(second_ptr)?;
1833    Ok((first, second))
1834}
1835
1836fn extract_meta_string<TCFunction>(
1837    c_function: TCFunction,
1838    capacity: usize,
1839) -> Result<String, MetaValError>
1840where
1841    TCFunction: Fn(*mut c_char, usize) -> i32,
1842{
1843    let mut buffer = vec![0u8; capacity];
1844    let result = c_function(buffer.as_mut_ptr().cast::<c_char>(), buffer.len());
1845
1846    if result < 0 {
1847        return Err(MetaValError::NegativeReturn(result));
1848    }
1849
1850    let returned_len = result.cast_unsigned() as usize;
1851
1852    if returned_len >= capacity {
1853        return extract_meta_string(c_function, returned_len + 1);
1854    }
1855
1856    if buffer.get(returned_len) != Some(&0) {
1857        return Err(MetaValError::NegativeReturn(-1));
1858    }
1859
1860    buffer.truncate(returned_len);
1861
1862    Ok(String::from_utf8(buffer)?)
1863}
1864
1865impl Drop for LlamaModel {
1866    fn drop(&mut self) {
1867        unsafe { llama_cpp_bindings_sys::llama_free_model(self.model.as_ptr()) }
1868    }
1869}
1870
1871#[cfg(test)]
1872mod extract_meta_string_tests {
1873    use super::extract_meta_string;
1874    use crate::MetaValError;
1875
1876    #[test]
1877    fn returns_error_when_null_terminator_missing() {
1878        let result = extract_meta_string(
1879            |buf_ptr, buf_len| {
1880                let buffer =
1881                    unsafe { std::slice::from_raw_parts_mut(buf_ptr.cast::<u8>(), buf_len) };
1882                buffer[0] = b'a';
1883                buffer[1] = b'b';
1884                buffer[2] = b'c';
1885                2
1886            },
1887            4,
1888        );
1889
1890        assert_eq!(result.unwrap_err(), MetaValError::NegativeReturn(-1));
1891    }
1892
1893    #[test]
1894    fn returns_error_for_negative_return_value() {
1895        let result = extract_meta_string(|_buf_ptr, _buf_len| -5, 4);
1896
1897        assert_eq!(result.unwrap_err(), MetaValError::NegativeReturn(-5));
1898    }
1899
1900    #[test]
1901    fn returns_error_for_invalid_utf8_data() {
1902        let result = extract_meta_string(
1903            |buf_ptr, buf_len| {
1904                let buffer =
1905                    unsafe { std::slice::from_raw_parts_mut(buf_ptr.cast::<u8>(), buf_len) };
1906                buffer[0] = 0xFF;
1907                buffer[1] = 0xFE;
1908                buffer[2] = 0;
1909                2
1910            },
1911            4,
1912        );
1913
1914        assert!(result.is_err());
1915        assert!(result.unwrap_err().to_string().contains("FromUtf8Error"));
1916    }
1917
1918    #[test]
1919    fn triggers_buffer_resize_when_returned_len_exceeds_capacity() {
1920        let initial_capacity: usize = 4;
1921        let length_exceeding_initial_capacity = 10;
1922        let written_length = 2;
1923        let call_count = std::cell::Cell::new(0);
1924        let result = extract_meta_string(
1925            |buf_ptr, buf_len| {
1926                let count = call_count.get();
1927                call_count.set(count + 1);
1928                if count == 0 {
1929                    length_exceeding_initial_capacity
1930                } else {
1931                    let buffer =
1932                        unsafe { std::slice::from_raw_parts_mut(buf_ptr.cast::<u8>(), buf_len) };
1933                    buffer[0] = b'h';
1934                    buffer[1] = b'i';
1935                    buffer[2] = 0;
1936                    written_length
1937                }
1938            },
1939            initial_capacity,
1940        );
1941
1942        assert_eq!(result.unwrap(), "hi");
1943    }
1944
1945    #[test]
1946    fn cstring_with_validated_len_null_byte_returns_error() {
1947        let result = super::cstring_with_validated_len("null\0byte");
1948
1949        assert!(result.is_err());
1950    }
1951
1952    #[test]
1953    fn validate_string_length_overflow_returns_error() {
1954        let result = super::validate_string_length_for_tokenizer(usize::MAX);
1955
1956        assert!(result.is_err());
1957    }
1958
1959    #[test]
1960    fn checked_token_buffer_capacity_overflow_returns_error() {
1961        assert!(super::checked_token_buffer_capacity(usize::MAX).is_err());
1962    }
1963
1964    #[test]
1965    fn checked_token_buffer_capacity_in_range_returns_value() {
1966        assert_eq!(super::checked_token_buffer_capacity(8), Ok(8));
1967    }
1968
1969    #[test]
1970    fn checked_token_count_negative_returns_error() {
1971        assert!(super::checked_token_count(-1).is_err());
1972    }
1973
1974    #[test]
1975    fn checked_token_count_non_negative_returns_value() {
1976        assert_eq!(super::checked_token_count(5), Ok(5));
1977    }
1978
1979    #[test]
1980    fn tokenize_into_buffer_single_pass_sets_length() {
1981        let buffer = super::tokenize_into_buffer(8, |_tokens, _n_tokens_max| Ok(3)).unwrap();
1982
1983        assert_eq!(buffer.len(), 3);
1984    }
1985
1986    #[test]
1987    fn tokenize_into_buffer_grows_buffer_when_first_pass_reports_negative_size() {
1988        let call_count = std::cell::Cell::new(0);
1989        let buffer = super::tokenize_into_buffer(8, |_tokens, _n_tokens_max| {
1990            let count = call_count.get();
1991            call_count.set(count + 1);
1992            if count == 0 { Ok(-20) } else { Ok(15) }
1993        })
1994        .unwrap();
1995
1996        assert_eq!(buffer.len(), 15);
1997        assert_eq!(call_count.get(), 2);
1998    }
1999
2000    #[test]
2001    fn tokenize_into_buffer_propagates_invocation_error() {
2002        let result = super::tokenize_into_buffer(8, |_tokens, _n_tokens_max| {
2003            Err(crate::StringToTokenError::NotEnoughMemory)
2004        });
2005
2006        assert_eq!(result, Err(crate::StringToTokenError::NotEnoughMemory));
2007    }
2008
2009    #[test]
2010    fn tokenize_into_buffer_propagates_second_invocation_error() {
2011        let call_count = std::cell::Cell::new(0);
2012        let result = super::tokenize_into_buffer(8, |_tokens, _n_tokens_max| {
2013            let count = call_count.get();
2014            call_count.set(count + 1);
2015            if count == 0 {
2016                Ok(-20)
2017            } else {
2018                Err(crate::StringToTokenError::NotEnoughMemory)
2019            }
2020        });
2021
2022        assert_eq!(result, Err(crate::StringToTokenError::NotEnoughMemory));
2023        assert_eq!(call_count.get(), 2);
2024    }
2025
2026    #[test]
2027    fn tokenize_into_buffer_negative_final_size_returns_conversion_error() {
2028        let call_count = std::cell::Cell::new(0);
2029        let result = super::tokenize_into_buffer(8, |_tokens, _n_tokens_max| {
2030            let count = call_count.get();
2031            call_count.set(count + 1);
2032            if count == 0 { Ok(-20) } else { Ok(-5) }
2033        });
2034
2035        assert_eq!(
2036            result.unwrap_err(),
2037            crate::StringToTokenError::CIntConversionError(usize::try_from(-5i32).unwrap_err())
2038        );
2039    }
2040
2041    #[test]
2042    fn read_optional_owned_cstr_invalid_utf8_returns_error() {
2043        let invalid_utf8_with_terminator: [u8; 3] = [0xFF, 0xFE, 0x00];
2044        let result = super::read_optional_owned_cstr(
2045            invalid_utf8_with_terminator
2046                .as_ptr()
2047                .cast::<std::ffi::c_char>(),
2048        );
2049
2050        assert_eq!(
2051            result.unwrap_err(),
2052            crate::MarkerDetectionError::MarkerUtf8Error(
2053                String::from_utf8(vec![0xFF, 0xFE]).unwrap_err()
2054            )
2055        );
2056    }
2057
2058    #[test]
2059    fn collect_optional_cstr_pair_first_invalid_utf8_returns_error() {
2060        let invalid_utf8_with_terminator: [u8; 3] = [0xFF, 0xFE, 0x00];
2061        let valid_with_terminator: [u8; 3] = [b'o', b'k', 0x00];
2062        let result = super::collect_optional_cstr_pair(
2063            invalid_utf8_with_terminator
2064                .as_ptr()
2065                .cast::<std::ffi::c_char>(),
2066            valid_with_terminator.as_ptr().cast::<std::ffi::c_char>(),
2067        );
2068
2069        assert_eq!(
2070            result.unwrap_err(),
2071            crate::MarkerDetectionError::MarkerUtf8Error(
2072                String::from_utf8(vec![0xFF, 0xFE]).unwrap_err()
2073            )
2074        );
2075    }
2076
2077    #[test]
2078    fn collect_optional_cstr_pair_second_invalid_utf8_returns_error() {
2079        let valid_with_terminator: [u8; 3] = [b'o', b'k', 0x00];
2080        let invalid_utf8_with_terminator: [u8; 3] = [0xFF, 0xFE, 0x00];
2081        let result = super::collect_optional_cstr_pair(
2082            valid_with_terminator.as_ptr().cast::<std::ffi::c_char>(),
2083            invalid_utf8_with_terminator
2084                .as_ptr()
2085                .cast::<std::ffi::c_char>(),
2086        );
2087
2088        assert_eq!(
2089            result.unwrap_err(),
2090            crate::MarkerDetectionError::MarkerUtf8Error(
2091                String::from_utf8(vec![0xFF, 0xFE]).unwrap_err()
2092            )
2093        );
2094    }
2095}
2096
2097#[cfg(test)]
2098mod ffi_status_mapping_tests {
2099    use std::ffi::c_char;
2100    use std::mem::discriminant;
2101    use std::path::Path;
2102    use std::ptr;
2103
2104    use llama_cpp_bindings_types::ParsedChatMessage;
2105    use llama_cpp_bindings_types::ParsedToolCall;
2106    use llama_cpp_bindings_types::ReasoningMarkers;
2107    use llama_cpp_bindings_types::ToolCallArguments;
2108
2109    use super::ReasoningSplit;
2110    use super::chat_parser_create_status_to_result;
2111    use super::compute_tool_call_haystack_status_to_result;
2112    use super::cxx_exception_owns_out_error;
2113    use super::detect_reasoning_markers_status_to_result;
2114    use super::diagnose_tool_call_synthetic_renders_status_to_result;
2115    use super::load_model_from_file_status_to_result;
2116    use super::outcome_from_via_ffi_result;
2117    use super::parse_chat_message_status_to_result;
2118    use super::parsed_chat_content_status_to_result;
2119    use super::parsed_chat_free_status_to_result;
2120    use super::parsed_chat_reasoning_content_status_to_result;
2121    use super::parsed_chat_tool_call_arguments_status_to_result;
2122    use super::parsed_chat_tool_call_count_status_to_result;
2123    use super::parsed_chat_tool_call_id_status_to_result;
2124    use super::parsed_chat_tool_call_name_status_to_result;
2125    use super::reasoning_markers_from_marker_pair;
2126    use super::render_chat_template_status_to_result;
2127    use super::split_reasoning_prefix;
2128    use super::tokenize_status_to_result;
2129    use crate::ChatMessageParseOutcome;
2130    use crate::LlamaModelLoadError;
2131    use crate::MarkerDetectionError;
2132    use crate::ParseChatMessageError;
2133    use crate::RawChatMessage;
2134    use crate::StringToTokenError;
2135
2136    #[test]
2137    fn cxx_exception_owns_out_error_classifies_each_failure_variant() {
2138        assert!(cxx_exception_owns_out_error::<()>(&Err(
2139            MarkerDetectionError::ReasoningMarkerDetectionFailed {
2140                message: String::new()
2141            }
2142        )));
2143        assert!(cxx_exception_owns_out_error::<()>(&Err(
2144            MarkerDetectionError::ToolCallHaystackComputationFailed {
2145                message: String::new()
2146            }
2147        )));
2148        assert!(cxx_exception_owns_out_error::<()>(&Err(
2149            MarkerDetectionError::ToolCallSyntheticRenderDiagnosisFailed {
2150                message: String::new()
2151            }
2152        )));
2153        assert!(!cxx_exception_owns_out_error::<()>(&Ok(())));
2154    }
2155
2156    #[test]
2157    fn load_model_from_file_ok_with_null_model_is_unloadable() {
2158        let result = unsafe {
2159            load_model_from_file_status_to_result(
2160                llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_OK,
2161                ptr::null_mut(),
2162                ptr::null_mut(),
2163                Path::new("/some/path"),
2164            )
2165        };
2166
2167        assert_eq!(result.unwrap_err(), LlamaModelLoadError::Unloadable);
2168    }
2169
2170    #[test]
2171    fn load_model_from_file_vendored_returned_null_for_missing_path_is_file_not_found() {
2172        let result = unsafe {
2173            load_model_from_file_status_to_result(
2174                llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_VENDORED_RETURNED_NULL,
2175                ptr::null_mut(),
2176                ptr::null_mut(),
2177                Path::new("/definitely/missing/model.gguf"),
2178            )
2179        };
2180
2181        assert_eq!(
2182            result.unwrap_err(),
2183            LlamaModelLoadError::FileNotFound(
2184                Path::new("/definitely/missing/model.gguf").to_path_buf()
2185            )
2186        );
2187    }
2188
2189    #[test]
2190    fn load_model_from_file_allocation_failed_is_not_enough_memory() {
2191        let result = unsafe {
2192            load_model_from_file_status_to_result(
2193                llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_ERROR_STRING_ALLOCATION_FAILED,
2194                ptr::null_mut(),
2195                ptr::null_mut(),
2196                Path::new("/some/path"),
2197            )
2198        };
2199
2200        assert_eq!(result.unwrap_err(), LlamaModelLoadError::NotEnoughMemory);
2201    }
2202
2203    #[test]
2204    fn load_model_from_file_cxx_exception_is_reported() {
2205        let result = unsafe {
2206            load_model_from_file_status_to_result(
2207                llama_cpp_bindings_sys::LLAMA_RS_LOAD_MODEL_FROM_FILE_VENDORED_THREW_CXX_EXCEPTION,
2208                ptr::null_mut(),
2209                ptr::null_mut(),
2210                Path::new("/some/path"),
2211            )
2212        };
2213
2214        assert_eq!(
2215            result.unwrap_err(),
2216            LlamaModelLoadError::Reported {
2217                message: "unknown error".to_owned()
2218            }
2219        );
2220    }
2221
2222    #[test]
2223    #[should_panic(expected = "llama_rs_load_model_from_file returned unrecognized status")]
2224    fn load_model_from_file_unrecognized_status_panics() {
2225        let _ = unsafe {
2226            load_model_from_file_status_to_result(
2227                llama_cpp_bindings_sys::llama_rs_load_model_from_file_status::MAX,
2228                ptr::null_mut(),
2229                ptr::null_mut(),
2230                Path::new("/some/path"),
2231            )
2232        };
2233    }
2234
2235    #[test]
2236    fn parse_chat_message_ok_with_null_handle_is_default_message() {
2237        let mut out_error: *mut c_char = ptr::null_mut();
2238        let result = unsafe {
2239            parse_chat_message_status_to_result(
2240                llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_OK,
2241                ptr::null_mut(),
2242                &raw mut out_error,
2243            )
2244        };
2245
2246        assert_eq!(result.unwrap(), ParsedChatMessage::default());
2247    }
2248
2249    #[test]
2250    fn chat_parser_create_no_chat_template_maps_to_no_chat_template() {
2251        let mut out_error: *mut c_char = ptr::null_mut();
2252        let result = unsafe {
2253            chat_parser_create_status_to_result(
2254                llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_MODEL_HAS_NO_CHAT_TEMPLATE,
2255                ptr::null_mut(),
2256                &raw mut out_error,
2257            )
2258        };
2259
2260        assert_eq!(
2261            discriminant(&result.unwrap_err()),
2262            discriminant(&ParseChatMessageError::NoChatTemplate)
2263        );
2264    }
2265
2266    #[test]
2267    fn chat_parser_create_no_vocab_maps_to_no_vocab() {
2268        let mut out_error: *mut c_char = ptr::null_mut();
2269        let result = unsafe {
2270            chat_parser_create_status_to_result(
2271                llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_MODEL_HAS_NO_VOCAB,
2272                ptr::null_mut(),
2273                &raw mut out_error,
2274            )
2275        };
2276
2277        assert_eq!(
2278            discriminant(&result.unwrap_err()),
2279            discriminant(&ParseChatMessageError::NoVocab)
2280        );
2281    }
2282
2283    #[test]
2284    fn chat_parser_create_allocation_failed_is_not_enough_memory() {
2285        let mut out_error: *mut c_char = ptr::null_mut();
2286        let result = unsafe {
2287            chat_parser_create_status_to_result(
2288                llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_ERROR_STRING_ALLOCATION_FAILED,
2289                ptr::null_mut(),
2290                &raw mut out_error,
2291            )
2292        };
2293
2294        assert_eq!(
2295            discriminant(&result.unwrap_err()),
2296            discriminant(&ParseChatMessageError::NotEnoughMemory)
2297        );
2298    }
2299
2300    #[test]
2301    fn chat_parser_create_cxx_exception_is_parse_failed_and_nulls_error() {
2302        let mut out_error: *mut c_char = ptr::null_mut();
2303        let result = unsafe {
2304            chat_parser_create_status_to_result(
2305                llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_VENDORED_THREW_CXX_EXCEPTION,
2306                ptr::null_mut(),
2307                &raw mut out_error,
2308            )
2309        };
2310
2311        assert_eq!(
2312            discriminant(&result.unwrap_err()),
2313            discriminant(&ParseChatMessageError::ParseFailed {
2314                message: String::new()
2315            })
2316        );
2317        assert!(out_error.is_null());
2318    }
2319
2320    #[test]
2321    #[should_panic(expected = "llama_rs_chat_parser_create returned OK with a null parser handle")]
2322    fn chat_parser_create_ok_with_null_parser_panics() {
2323        let mut out_error: *mut c_char = ptr::null_mut();
2324        let _ = unsafe {
2325            chat_parser_create_status_to_result(
2326                llama_cpp_bindings_sys::LLAMA_RS_CHAT_PARSER_CREATE_OK,
2327                ptr::null_mut(),
2328                &raw mut out_error,
2329            )
2330        };
2331    }
2332
2333    #[test]
2334    #[should_panic(expected = "llama_rs_chat_parser_create returned unrecognized status")]
2335    fn chat_parser_create_unrecognized_status_panics() {
2336        let mut out_error: *mut c_char = ptr::null_mut();
2337        let _ = unsafe {
2338            chat_parser_create_status_to_result(
2339                llama_cpp_bindings_sys::llama_rs_chat_parser_create_status::MAX,
2340                ptr::null_mut(),
2341                &raw mut out_error,
2342            )
2343        };
2344    }
2345
2346    #[test]
2347    fn parse_chat_message_allocation_failed_is_not_enough_memory() {
2348        let mut out_error: *mut c_char = ptr::null_mut();
2349        let result = unsafe {
2350            parse_chat_message_status_to_result(
2351                llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_ERROR_STRING_ALLOCATION_FAILED,
2352                ptr::null_mut(),
2353                &raw mut out_error,
2354            )
2355        };
2356
2357        assert_eq!(
2358            discriminant(&result.unwrap_err()),
2359            discriminant(&ParseChatMessageError::NotEnoughMemory)
2360        );
2361    }
2362
2363    #[test]
2364    fn parse_chat_message_cxx_exception_is_parse_failed_and_nulls_error() {
2365        let mut out_error: *mut c_char = ptr::null_mut();
2366        let result = unsafe {
2367            parse_chat_message_status_to_result(
2368                llama_cpp_bindings_sys::LLAMA_RS_PARSE_CHAT_MESSAGE_VENDORED_THREW_CXX_EXCEPTION,
2369                ptr::null_mut(),
2370                &raw mut out_error,
2371            )
2372        };
2373
2374        assert_eq!(
2375            discriminant(&result.unwrap_err()),
2376            discriminant(&ParseChatMessageError::ParseFailed {
2377                message: String::new()
2378            })
2379        );
2380        assert!(out_error.is_null());
2381    }
2382
2383    #[test]
2384    #[should_panic(expected = "llama_rs_parse_chat_message returned unrecognized status")]
2385    fn parse_chat_message_unrecognized_status_panics() {
2386        let mut out_error: *mut c_char = ptr::null_mut();
2387        let _ = unsafe {
2388            parse_chat_message_status_to_result(
2389                llama_cpp_bindings_sys::llama_rs_parse_chat_message_status::MAX,
2390                ptr::null_mut(),
2391                &raw mut out_error,
2392            )
2393        };
2394    }
2395
2396    #[test]
2397    fn parsed_chat_free_ok_returns_parsed_value() {
2398        let parsed = Ok(ParsedChatMessage::default());
2399        let result = unsafe {
2400            parsed_chat_free_status_to_result(
2401                parsed,
2402                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_OK,
2403                ptr::null_mut(),
2404                ptr::null_mut(),
2405            )
2406        };
2407
2408        assert_eq!(result.unwrap(), ParsedChatMessage::default());
2409    }
2410
2411    #[test]
2412    fn parsed_chat_free_destructor_threw_is_destructor_failed() {
2413        let parsed = Ok(ParsedChatMessage::default());
2414        let result = unsafe {
2415            parsed_chat_free_status_to_result(
2416                parsed,
2417                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_DESTRUCTOR_THREW_CXX_EXCEPTION,
2418                ptr::null_mut(),
2419                ptr::null_mut(),
2420            )
2421        };
2422
2423        assert_eq!(
2424            discriminant(&result.unwrap_err()),
2425            discriminant(&ParseChatMessageError::DestructorFailed {
2426                message: String::new()
2427            })
2428        );
2429    }
2430
2431    #[test]
2432    fn parsed_chat_free_allocation_failed_is_not_enough_memory() {
2433        let parsed = Ok(ParsedChatMessage::default());
2434        let result = unsafe {
2435            parsed_chat_free_status_to_result(
2436                parsed,
2437                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_ERROR_STRING_ALLOCATION_FAILED,
2438                ptr::null_mut(),
2439                ptr::null_mut(),
2440            )
2441        };
2442
2443        assert_eq!(
2444            discriminant(&result.unwrap_err()),
2445            discriminant(&ParseChatMessageError::NotEnoughMemory)
2446        );
2447    }
2448
2449    #[test]
2450    fn parsed_chat_free_propagates_existing_parse_error() {
2451        let parsed = Err(ParseChatMessageError::NoVocab);
2452        let result = unsafe {
2453            parsed_chat_free_status_to_result(
2454                parsed,
2455                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_FREE_OK,
2456                ptr::null_mut(),
2457                ptr::null_mut(),
2458            )
2459        };
2460
2461        assert_eq!(
2462            discriminant(&result.unwrap_err()),
2463            discriminant(&ParseChatMessageError::NoVocab)
2464        );
2465    }
2466
2467    #[test]
2468    #[should_panic(expected = "llama_rs_parsed_chat_free returned unrecognized status")]
2469    fn parsed_chat_free_unrecognized_status_panics() {
2470        let parsed = Ok(ParsedChatMessage::default());
2471        let _ = unsafe {
2472            parsed_chat_free_status_to_result(
2473                parsed,
2474                llama_cpp_bindings_sys::llama_rs_parsed_chat_free_status::MAX,
2475                ptr::null_mut(),
2476                ptr::null_mut(),
2477            )
2478        };
2479    }
2480
2481    #[test]
2482    fn parsed_chat_content_ok_with_null_string_is_empty() {
2483        let result = unsafe {
2484            parsed_chat_content_status_to_result(
2485                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_OK,
2486                ptr::null_mut(),
2487                ptr::null_mut(),
2488            )
2489        };
2490
2491        assert_eq!(result.unwrap(), "");
2492    }
2493
2494    #[test]
2495    fn parsed_chat_content_allocation_failed_is_not_enough_memory() {
2496        let result = unsafe {
2497            parsed_chat_content_status_to_result(
2498                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_ERROR_STRING_ALLOCATION_FAILED,
2499                ptr::null_mut(),
2500                ptr::null_mut(),
2501            )
2502        };
2503
2504        assert_eq!(
2505            discriminant(&result.unwrap_err()),
2506            discriminant(&ParseChatMessageError::NotEnoughMemory)
2507        );
2508    }
2509
2510    #[test]
2511    fn parsed_chat_content_cxx_exception_is_reported() {
2512        let result = unsafe {
2513            parsed_chat_content_status_to_result(
2514                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_CONTENT_VENDORED_THREW_CXX_EXCEPTION,
2515                ptr::null_mut(),
2516                ptr::null_mut(),
2517            )
2518        };
2519
2520        assert_eq!(
2521            discriminant(&result.unwrap_err()),
2522            discriminant(&ParseChatMessageError::Reported {
2523                message: String::new()
2524            })
2525        );
2526    }
2527
2528    #[test]
2529    #[should_panic(expected = "llama_rs_parsed_chat_content returned unrecognized status")]
2530    fn parsed_chat_content_unrecognized_status_panics() {
2531        let _ = unsafe {
2532            parsed_chat_content_status_to_result(
2533                llama_cpp_bindings_sys::llama_rs_parsed_chat_content_status::MAX,
2534                ptr::null_mut(),
2535                ptr::null_mut(),
2536            )
2537        };
2538    }
2539
2540    #[test]
2541    fn parsed_chat_reasoning_content_ok_with_null_string_is_empty() {
2542        let result = unsafe {
2543            parsed_chat_reasoning_content_status_to_result(
2544                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_OK,
2545                ptr::null_mut(),
2546                ptr::null_mut(),
2547            )
2548        };
2549
2550        assert_eq!(result.unwrap(), "");
2551    }
2552
2553    #[test]
2554    fn parsed_chat_reasoning_content_allocation_failed_is_not_enough_memory() {
2555        let result = unsafe {
2556            parsed_chat_reasoning_content_status_to_result(
2557                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_ERROR_STRING_ALLOCATION_FAILED,
2558                ptr::null_mut(),
2559                ptr::null_mut(),
2560            )
2561        };
2562
2563        assert_eq!(
2564            discriminant(&result.unwrap_err()),
2565            discriminant(&ParseChatMessageError::NotEnoughMemory)
2566        );
2567    }
2568
2569    #[test]
2570    fn parsed_chat_reasoning_content_cxx_exception_is_reported() {
2571        let result = unsafe {
2572            parsed_chat_reasoning_content_status_to_result(
2573                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_REASONING_CONTENT_VENDORED_THREW_CXX_EXCEPTION,
2574                ptr::null_mut(),
2575                ptr::null_mut(),
2576            )
2577        };
2578
2579        assert_eq!(
2580            discriminant(&result.unwrap_err()),
2581            discriminant(&ParseChatMessageError::Reported {
2582                message: String::new()
2583            })
2584        );
2585    }
2586
2587    #[test]
2588    #[should_panic(
2589        expected = "llama_rs_parsed_chat_reasoning_content returned unrecognized status"
2590    )]
2591    fn parsed_chat_reasoning_content_unrecognized_status_panics() {
2592        let _ = unsafe {
2593            parsed_chat_reasoning_content_status_to_result(
2594                llama_cpp_bindings_sys::llama_rs_parsed_chat_reasoning_content_status::MAX,
2595                ptr::null_mut(),
2596                ptr::null_mut(),
2597            )
2598        };
2599    }
2600
2601    #[test]
2602    fn parsed_chat_tool_call_count_ok_returns_count() {
2603        let result = unsafe {
2604            parsed_chat_tool_call_count_status_to_result(
2605                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_OK,
2606                7,
2607                ptr::null_mut(),
2608            )
2609        };
2610
2611        assert_eq!(result.unwrap(), 7);
2612    }
2613
2614    #[test]
2615    fn parsed_chat_tool_call_count_allocation_failed_is_not_enough_memory() {
2616        let result = unsafe {
2617            parsed_chat_tool_call_count_status_to_result(
2618                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_ERROR_STRING_ALLOCATION_FAILED,
2619                0,
2620                ptr::null_mut(),
2621            )
2622        };
2623
2624        assert_eq!(
2625            discriminant(&result.unwrap_err()),
2626            discriminant(&ParseChatMessageError::NotEnoughMemory)
2627        );
2628    }
2629
2630    #[test]
2631    fn parsed_chat_tool_call_count_cxx_exception_is_reported() {
2632        let result = unsafe {
2633            parsed_chat_tool_call_count_status_to_result(
2634                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_COUNT_VENDORED_THREW_CXX_EXCEPTION,
2635                0,
2636                ptr::null_mut(),
2637            )
2638        };
2639
2640        assert_eq!(
2641            discriminant(&result.unwrap_err()),
2642            discriminant(&ParseChatMessageError::Reported {
2643                message: String::new()
2644            })
2645        );
2646    }
2647
2648    #[test]
2649    #[should_panic(expected = "llama_rs_parsed_chat_tool_call_count returned unrecognized status")]
2650    fn parsed_chat_tool_call_count_unrecognized_status_panics() {
2651        let _ = unsafe {
2652            parsed_chat_tool_call_count_status_to_result(
2653                llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_count_status::MAX,
2654                0,
2655                ptr::null_mut(),
2656            )
2657        };
2658    }
2659
2660    #[test]
2661    fn parsed_chat_tool_call_id_ok_with_null_string_is_empty() {
2662        let result = unsafe {
2663            parsed_chat_tool_call_id_status_to_result(
2664                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_OK,
2665                0,
2666                ptr::null_mut(),
2667                ptr::null_mut(),
2668            )
2669        };
2670
2671        assert_eq!(result.unwrap(), "");
2672    }
2673
2674    #[test]
2675    fn parsed_chat_tool_call_id_out_of_bounds_carries_index() {
2676        let result = unsafe {
2677            parsed_chat_tool_call_id_status_to_result(
2678                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_INDEX_OUT_OF_BOUNDS,
2679                4,
2680                ptr::null_mut(),
2681                ptr::null_mut(),
2682            )
2683        };
2684
2685        let Err(ParseChatMessageError::ToolCallIdIndexOutOfBounds { index }) = result else {
2686            panic!("expected ToolCallIdIndexOutOfBounds, got {result:?}");
2687        };
2688        assert_eq!(index, 4);
2689    }
2690
2691    #[test]
2692    fn parsed_chat_tool_call_id_allocation_failed_is_not_enough_memory() {
2693        let result = unsafe {
2694            parsed_chat_tool_call_id_status_to_result(
2695                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_ERROR_STRING_ALLOCATION_FAILED,
2696                0,
2697                ptr::null_mut(),
2698                ptr::null_mut(),
2699            )
2700        };
2701
2702        assert_eq!(
2703            discriminant(&result.unwrap_err()),
2704            discriminant(&ParseChatMessageError::NotEnoughMemory)
2705        );
2706    }
2707
2708    #[test]
2709    fn parsed_chat_tool_call_id_cxx_exception_is_reported() {
2710        let result = unsafe {
2711            parsed_chat_tool_call_id_status_to_result(
2712                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ID_VENDORED_THREW_CXX_EXCEPTION,
2713                0,
2714                ptr::null_mut(),
2715                ptr::null_mut(),
2716            )
2717        };
2718
2719        assert_eq!(
2720            discriminant(&result.unwrap_err()),
2721            discriminant(&ParseChatMessageError::Reported {
2722                message: String::new()
2723            })
2724        );
2725    }
2726
2727    #[test]
2728    #[should_panic(expected = "llama_rs_parsed_chat_tool_call_id returned unrecognized status")]
2729    fn parsed_chat_tool_call_id_unrecognized_status_panics() {
2730        let _ = unsafe {
2731            parsed_chat_tool_call_id_status_to_result(
2732                llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_id_status::MAX,
2733                0,
2734                ptr::null_mut(),
2735                ptr::null_mut(),
2736            )
2737        };
2738    }
2739
2740    #[test]
2741    fn parsed_chat_tool_call_name_ok_with_null_string_is_empty() {
2742        let result = unsafe {
2743            parsed_chat_tool_call_name_status_to_result(
2744                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_OK,
2745                0,
2746                ptr::null_mut(),
2747                ptr::null_mut(),
2748            )
2749        };
2750
2751        assert_eq!(result.unwrap(), "");
2752    }
2753
2754    #[test]
2755    fn parsed_chat_tool_call_name_out_of_bounds_carries_index() {
2756        let result = unsafe {
2757            parsed_chat_tool_call_name_status_to_result(
2758                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_INDEX_OUT_OF_BOUNDS,
2759                2,
2760                ptr::null_mut(),
2761                ptr::null_mut(),
2762            )
2763        };
2764
2765        let Err(ParseChatMessageError::ToolCallNameIndexOutOfBounds { index }) = result else {
2766            panic!("expected ToolCallNameIndexOutOfBounds, got {result:?}");
2767        };
2768        assert_eq!(index, 2);
2769    }
2770
2771    #[test]
2772    fn parsed_chat_tool_call_name_allocation_failed_is_not_enough_memory() {
2773        let result = unsafe {
2774            parsed_chat_tool_call_name_status_to_result(
2775                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_ERROR_STRING_ALLOCATION_FAILED,
2776                0,
2777                ptr::null_mut(),
2778                ptr::null_mut(),
2779            )
2780        };
2781
2782        assert_eq!(
2783            discriminant(&result.unwrap_err()),
2784            discriminant(&ParseChatMessageError::NotEnoughMemory)
2785        );
2786    }
2787
2788    #[test]
2789    fn parsed_chat_tool_call_name_cxx_exception_is_reported() {
2790        let result = unsafe {
2791            parsed_chat_tool_call_name_status_to_result(
2792                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_NAME_VENDORED_THREW_CXX_EXCEPTION,
2793                0,
2794                ptr::null_mut(),
2795                ptr::null_mut(),
2796            )
2797        };
2798
2799        assert_eq!(
2800            discriminant(&result.unwrap_err()),
2801            discriminant(&ParseChatMessageError::Reported {
2802                message: String::new()
2803            })
2804        );
2805    }
2806
2807    #[test]
2808    #[should_panic(expected = "llama_rs_parsed_chat_tool_call_name returned unrecognized status")]
2809    fn parsed_chat_tool_call_name_unrecognized_status_panics() {
2810        let _ = unsafe {
2811            parsed_chat_tool_call_name_status_to_result(
2812                llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_name_status::MAX,
2813                0,
2814                ptr::null_mut(),
2815                ptr::null_mut(),
2816            )
2817        };
2818    }
2819
2820    #[test]
2821    fn parsed_chat_tool_call_arguments_ok_with_null_string_is_empty() {
2822        let result = unsafe {
2823            parsed_chat_tool_call_arguments_status_to_result(
2824                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_OK,
2825                0,
2826                ptr::null_mut(),
2827                ptr::null_mut(),
2828            )
2829        };
2830
2831        assert_eq!(result.unwrap(), "");
2832    }
2833
2834    #[test]
2835    fn parsed_chat_tool_call_arguments_out_of_bounds_carries_index() {
2836        let result = unsafe {
2837            parsed_chat_tool_call_arguments_status_to_result(
2838                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_INDEX_OUT_OF_BOUNDS,
2839                9,
2840                ptr::null_mut(),
2841                ptr::null_mut(),
2842            )
2843        };
2844
2845        let Err(ParseChatMessageError::ToolCallArgumentsIndexOutOfBounds { index }) = result else {
2846            panic!("expected ToolCallArgumentsIndexOutOfBounds, got {result:?}");
2847        };
2848        assert_eq!(index, 9);
2849    }
2850
2851    #[test]
2852    fn parsed_chat_tool_call_arguments_allocation_failed_is_not_enough_memory() {
2853        let result = unsafe {
2854            parsed_chat_tool_call_arguments_status_to_result(
2855                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_ERROR_STRING_ALLOCATION_FAILED,
2856                0,
2857                ptr::null_mut(),
2858                ptr::null_mut(),
2859            )
2860        };
2861
2862        assert_eq!(
2863            discriminant(&result.unwrap_err()),
2864            discriminant(&ParseChatMessageError::NotEnoughMemory)
2865        );
2866    }
2867
2868    #[test]
2869    fn parsed_chat_tool_call_arguments_cxx_exception_is_reported() {
2870        let result = unsafe {
2871            parsed_chat_tool_call_arguments_status_to_result(
2872                llama_cpp_bindings_sys::LLAMA_RS_PARSED_CHAT_TOOL_CALL_ARGUMENTS_VENDORED_THREW_CXX_EXCEPTION,
2873                0,
2874                ptr::null_mut(),
2875                ptr::null_mut(),
2876            )
2877        };
2878
2879        assert_eq!(
2880            discriminant(&result.unwrap_err()),
2881            discriminant(&ParseChatMessageError::Reported {
2882                message: String::new()
2883            })
2884        );
2885    }
2886
2887    #[test]
2888    #[should_panic(
2889        expected = "llama_rs_parsed_chat_tool_call_arguments returned unrecognized status"
2890    )]
2891    fn parsed_chat_tool_call_arguments_unrecognized_status_panics() {
2892        let _ = unsafe {
2893            parsed_chat_tool_call_arguments_status_to_result(
2894                llama_cpp_bindings_sys::llama_rs_parsed_chat_tool_call_arguments_status::MAX,
2895                0,
2896                ptr::null_mut(),
2897                ptr::null_mut(),
2898            )
2899        };
2900    }
2901
2902    #[test]
2903    fn render_chat_template_status_ok_reads_rendered() {
2904        let rendered = std::ffi::CString::new("hi").expect("test render string");
2905        let result = unsafe {
2906            render_chat_template_status_to_result(
2907                llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_OK,
2908                rendered.as_ptr(),
2909                ptr::null_mut(),
2910            )
2911        };
2912
2913        assert_eq!(result.expect("ok render"), Some("hi".to_owned()));
2914    }
2915
2916    #[test]
2917    fn render_chat_template_status_no_chat_template_is_none() {
2918        let result = unsafe {
2919            render_chat_template_status_to_result(
2920                llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_MODEL_HAS_NO_CHAT_TEMPLATE,
2921                ptr::null(),
2922                ptr::null_mut(),
2923            )
2924        };
2925
2926        assert_eq!(result.expect("none"), None);
2927    }
2928
2929    #[test]
2930    fn render_chat_template_status_no_vocab_is_none() {
2931        let result = unsafe {
2932            render_chat_template_status_to_result(
2933                llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_MODEL_HAS_NO_VOCAB,
2934                ptr::null(),
2935                ptr::null_mut(),
2936            )
2937        };
2938
2939        assert_eq!(result.expect("none"), None);
2940    }
2941
2942    #[test]
2943    fn render_chat_template_status_allocation_failed_is_not_enough_memory() {
2944        let result = unsafe {
2945            render_chat_template_status_to_result(
2946                llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_ERROR_STRING_ALLOCATION_FAILED,
2947                ptr::null(),
2948                ptr::null_mut(),
2949            )
2950        };
2951
2952        assert_eq!(
2953            discriminant(&result.unwrap_err()),
2954            discriminant(&MarkerDetectionError::NotEnoughMemory)
2955        );
2956    }
2957
2958    #[test]
2959    fn render_chat_template_status_cxx_exception_is_reported() {
2960        let result = unsafe {
2961            render_chat_template_status_to_result(
2962                llama_cpp_bindings_sys::LLAMA_RS_RENDER_CHAT_TEMPLATE_VENDORED_THREW_CXX_EXCEPTION,
2963                ptr::null(),
2964                ptr::null_mut(),
2965            )
2966        };
2967
2968        assert_eq!(
2969            discriminant(&result.unwrap_err()),
2970            discriminant(&MarkerDetectionError::ReasoningMarkerDetectionFailed {
2971                message: String::new()
2972            })
2973        );
2974    }
2975
2976    #[test]
2977    #[should_panic(expected = "llama_rs_render_chat_template returned unrecognized status")]
2978    fn render_chat_template_status_unrecognized_panics() {
2979        let _ = unsafe {
2980            render_chat_template_status_to_result(
2981                llama_cpp_bindings_sys::llama_rs_render_chat_template_status::MAX,
2982                ptr::null(),
2983                ptr::null_mut(),
2984            )
2985        };
2986    }
2987
2988    #[test]
2989    fn detect_reasoning_markers_ok_with_null_pointers_is_none_pair() {
2990        let result = unsafe {
2991            detect_reasoning_markers_status_to_result(
2992                llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_OK,
2993                ptr::null(),
2994                ptr::null(),
2995                ptr::null_mut(),
2996            )
2997        };
2998
2999        assert_eq!(result, Ok((None, None)));
3000    }
3001
3002    #[test]
3003    fn detect_reasoning_markers_allocation_failed_is_not_enough_memory() {
3004        let result = unsafe {
3005            detect_reasoning_markers_status_to_result(
3006                llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_ERROR_STRING_ALLOCATION_FAILED,
3007                ptr::null(),
3008                ptr::null(),
3009                ptr::null_mut(),
3010            )
3011        };
3012
3013        assert_eq!(result, Err(MarkerDetectionError::NotEnoughMemory));
3014    }
3015
3016    #[test]
3017    fn detect_reasoning_markers_cxx_exception_is_detection_failed() {
3018        let result = unsafe {
3019            detect_reasoning_markers_status_to_result(
3020                llama_cpp_bindings_sys::LLAMA_RS_DETECT_REASONING_MARKERS_VENDORED_THREW_CXX_EXCEPTION,
3021                ptr::null(),
3022                ptr::null(),
3023                ptr::null_mut(),
3024            )
3025        };
3026
3027        assert_eq!(
3028            result,
3029            Err(MarkerDetectionError::ReasoningMarkerDetectionFailed {
3030                message: "unknown error".to_owned()
3031            })
3032        );
3033    }
3034
3035    #[test]
3036    #[should_panic(expected = "llama_rs_detect_reasoning_markers returned unrecognized status")]
3037    fn detect_reasoning_markers_unrecognized_status_panics() {
3038        let _ = unsafe {
3039            detect_reasoning_markers_status_to_result(
3040                llama_cpp_bindings_sys::llama_rs_detect_reasoning_markers_status::MAX,
3041                ptr::null(),
3042                ptr::null(),
3043                ptr::null_mut(),
3044            )
3045        };
3046    }
3047
3048    #[test]
3049    fn compute_tool_call_haystack_ok_with_null_pointer_is_none() {
3050        let result = unsafe {
3051            compute_tool_call_haystack_status_to_result(
3052                llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_OK,
3053                ptr::null(),
3054                ptr::null_mut(),
3055            )
3056        };
3057
3058        assert_eq!(result, Ok(None));
3059    }
3060
3061    #[test]
3062    fn compute_tool_call_haystack_allocation_failed_is_not_enough_memory() {
3063        let result = unsafe {
3064            compute_tool_call_haystack_status_to_result(
3065                llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_ERROR_STRING_ALLOCATION_FAILED,
3066                ptr::null(),
3067                ptr::null_mut(),
3068            )
3069        };
3070
3071        assert_eq!(result, Err(MarkerDetectionError::NotEnoughMemory));
3072    }
3073
3074    #[test]
3075    fn compute_tool_call_haystack_cxx_exception_is_computation_failed() {
3076        let result = unsafe {
3077            compute_tool_call_haystack_status_to_result(
3078                llama_cpp_bindings_sys::LLAMA_RS_COMPUTE_TOOL_CALL_HAYSTACK_VENDORED_THREW_CXX_EXCEPTION,
3079                ptr::null(),
3080                ptr::null_mut(),
3081            )
3082        };
3083
3084        assert_eq!(
3085            result,
3086            Err(MarkerDetectionError::ToolCallHaystackComputationFailed {
3087                message: "unknown error".to_owned()
3088            })
3089        );
3090    }
3091
3092    #[test]
3093    #[should_panic(expected = "llama_rs_compute_tool_call_haystack returned unrecognized status")]
3094    fn compute_tool_call_haystack_unrecognized_status_panics() {
3095        let _ = unsafe {
3096            compute_tool_call_haystack_status_to_result(
3097                llama_cpp_bindings_sys::llama_rs_compute_tool_call_haystack_status::MAX,
3098                ptr::null(),
3099                ptr::null_mut(),
3100            )
3101        };
3102    }
3103
3104    #[test]
3105    fn diagnose_tool_call_synthetic_renders_ok_with_null_pointers_is_none_pair() {
3106        let result = unsafe {
3107            diagnose_tool_call_synthetic_renders_status_to_result(
3108                llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_OK,
3109                ptr::null(),
3110                ptr::null(),
3111                ptr::null_mut(),
3112            )
3113        };
3114
3115        assert_eq!(result, Ok((None, None)));
3116    }
3117
3118    #[test]
3119    fn diagnose_tool_call_synthetic_renders_allocation_failed_is_not_enough_memory() {
3120        let result = unsafe {
3121            diagnose_tool_call_synthetic_renders_status_to_result(
3122                llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_ERROR_STRING_ALLOCATION_FAILED,
3123                ptr::null(),
3124                ptr::null(),
3125                ptr::null_mut(),
3126            )
3127        };
3128
3129        assert_eq!(result, Err(MarkerDetectionError::NotEnoughMemory));
3130    }
3131
3132    #[test]
3133    fn diagnose_tool_call_synthetic_renders_cxx_exception_is_diagnosis_failed() {
3134        let result = unsafe {
3135            diagnose_tool_call_synthetic_renders_status_to_result(
3136                llama_cpp_bindings_sys::LLAMA_RS_DIAGNOSE_TOOL_CALL_SYNTHETIC_RENDERS_VENDORED_THREW_CXX_EXCEPTION,
3137                ptr::null(),
3138                ptr::null(),
3139                ptr::null_mut(),
3140            )
3141        };
3142
3143        assert_eq!(
3144            result,
3145            Err(
3146                MarkerDetectionError::ToolCallSyntheticRenderDiagnosisFailed {
3147                    message: "unknown error".to_owned()
3148                }
3149            )
3150        );
3151    }
3152
3153    #[test]
3154    #[should_panic(
3155        expected = "llama_rs_diagnose_tool_call_synthetic_renders returned unrecognized status"
3156    )]
3157    fn diagnose_tool_call_synthetic_renders_unrecognized_status_panics() {
3158        let _ = unsafe {
3159            diagnose_tool_call_synthetic_renders_status_to_result(
3160                llama_cpp_bindings_sys::llama_rs_diagnose_tool_call_synthetic_renders_status::MAX,
3161                ptr::null(),
3162                ptr::null(),
3163                ptr::null_mut(),
3164            )
3165        };
3166    }
3167
3168    #[test]
3169    fn tokenize_ok_returns_count() {
3170        let result = unsafe {
3171            tokenize_status_to_result(
3172                llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_OK,
3173                12,
3174                ptr::null_mut(),
3175            )
3176        };
3177
3178        assert_eq!(result, Ok(12));
3179    }
3180
3181    #[test]
3182    fn tokenize_allocation_failed_is_not_enough_memory() {
3183        let result = unsafe {
3184            tokenize_status_to_result(
3185                llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_ERROR_STRING_ALLOCATION_FAILED,
3186                0,
3187                ptr::null_mut(),
3188            )
3189        };
3190
3191        assert_eq!(result, Err(StringToTokenError::NotEnoughMemory));
3192    }
3193
3194    #[test]
3195    fn tokenize_cxx_exception_is_reported() {
3196        let result = unsafe {
3197            tokenize_status_to_result(
3198                llama_cpp_bindings_sys::LLAMA_RS_TOKENIZE_VENDORED_THREW_CXX_EXCEPTION,
3199                0,
3200                ptr::null_mut(),
3201            )
3202        };
3203
3204        assert_eq!(
3205            result,
3206            Err(StringToTokenError::Reported {
3207                message: "unknown error".to_owned()
3208            })
3209        );
3210    }
3211
3212    #[test]
3213    #[should_panic(expected = "llama_rs_tokenize returned unrecognized status")]
3214    fn tokenize_unrecognized_status_panics() {
3215        let _ = unsafe {
3216            tokenize_status_to_result(
3217                llama_cpp_bindings_sys::llama_rs_tokenize_status::MAX,
3218                0,
3219                ptr::null_mut(),
3220            )
3221        };
3222    }
3223
3224    #[test]
3225    fn apply_chat_template_ok_returns_rendered_prompt() {
3226        unsafe extern "C" {
3227            fn strdup(text: *const c_char) -> *mut c_char;
3228        }
3229        let rendered = std::ffi::CString::new("<bos>rendered prompt").unwrap();
3230        let out_string = unsafe { strdup(rendered.as_ptr()) };
3231        let result = unsafe {
3232            super::apply_chat_template_status_to_result(
3233                llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_OK,
3234                out_string,
3235                ptr::null_mut(),
3236            )
3237        };
3238
3239        assert_eq!(result, Ok("<bos>rendered prompt".to_owned()));
3240    }
3241
3242    #[test]
3243    fn apply_chat_template_no_vocab_maps_to_no_vocab() {
3244        let result = unsafe {
3245            super::apply_chat_template_status_to_result(
3246                llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_MODEL_HAS_NO_VOCAB,
3247                ptr::null_mut(),
3248                ptr::null_mut(),
3249            )
3250        };
3251
3252        assert_eq!(result, Err(crate::ApplyChatTemplateError::NoVocab));
3253    }
3254
3255    #[test]
3256    fn apply_chat_template_application_failed_maps_to_template_application_failed() {
3257        let result = unsafe {
3258            super::apply_chat_template_status_to_result(
3259                llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_TEMPLATE_APPLICATION_FAILED,
3260                ptr::null_mut(),
3261                ptr::null_mut(),
3262            )
3263        };
3264
3265        assert_eq!(
3266            result,
3267            Err(crate::ApplyChatTemplateError::TemplateApplicationFailed)
3268        );
3269    }
3270
3271    #[test]
3272    fn apply_chat_template_allocation_failed_maps_to_not_enough_memory() {
3273        let result = unsafe {
3274            super::apply_chat_template_status_to_result(
3275                llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_ERROR_STRING_ALLOCATION_FAILED,
3276                ptr::null_mut(),
3277                ptr::null_mut(),
3278            )
3279        };
3280
3281        assert_eq!(result, Err(crate::ApplyChatTemplateError::NotEnoughMemory));
3282    }
3283
3284    #[test]
3285    fn apply_chat_template_cxx_exception_is_reported() {
3286        unsafe extern "C" {
3287            fn strdup(text: *const c_char) -> *mut c_char;
3288        }
3289        let message = std::ffi::CString::new("renderer exploded").unwrap();
3290        let out_error = unsafe { strdup(message.as_ptr()) };
3291        let result = unsafe {
3292            super::apply_chat_template_status_to_result(
3293                llama_cpp_bindings_sys::LLAMA_RS_APPLY_CHAT_TEMPLATE_VENDORED_THREW_CXX_EXCEPTION,
3294                ptr::null_mut(),
3295                out_error,
3296            )
3297        };
3298
3299        assert_eq!(
3300            result,
3301            Err(crate::ApplyChatTemplateError::Reported {
3302                message: "renderer exploded".to_owned()
3303            })
3304        );
3305    }
3306
3307    #[test]
3308    #[should_panic(expected = "llama_rs_apply_chat_template returned unrecognized status")]
3309    fn apply_chat_template_unrecognized_status_panics() {
3310        let _ = unsafe {
3311            super::apply_chat_template_status_to_result(
3312                llama_cpp_bindings_sys::llama_rs_apply_chat_template_status::MAX,
3313                ptr::null_mut(),
3314                ptr::null_mut(),
3315            )
3316        };
3317    }
3318
3319    #[test]
3320    fn split_reasoning_prefix_without_markers_returns_content_up_to_tool_call_open() {
3321        let ReasoningSplit { reasoning, content } =
3322            split_reasoning_prefix("answer<tool>rest", None, "<tool>");
3323
3324        assert!(reasoning.is_empty());
3325        assert_eq!(content, "answer");
3326    }
3327
3328    #[test]
3329    fn split_reasoning_prefix_with_missing_open_marker_returns_content_only() {
3330        let markers = ReasoningMarkers {
3331            open: "<think>".to_owned(),
3332            close: "</think>".to_owned(),
3333        };
3334        let ReasoningSplit { reasoning, content } =
3335            split_reasoning_prefix("plain answer", Some(&markers), "<tool>");
3336
3337        assert!(reasoning.is_empty());
3338        assert_eq!(content, "plain answer");
3339    }
3340
3341    #[test]
3342    fn split_reasoning_prefix_with_missing_close_marker_returns_content_only() {
3343        let markers = ReasoningMarkers {
3344            open: "<think>".to_owned(),
3345            close: "</think>".to_owned(),
3346        };
3347        let ReasoningSplit { reasoning, content } =
3348            split_reasoning_prefix("<think>unterminated", Some(&markers), "<tool>");
3349
3350        assert!(reasoning.is_empty());
3351        assert_eq!(content, "<think>unterminated");
3352    }
3353
3354    #[test]
3355    fn split_reasoning_prefix_extracts_reasoning_and_trailing_content() {
3356        let markers = ReasoningMarkers {
3357            open: "<think>".to_owned(),
3358            close: "</think>".to_owned(),
3359        };
3360        let ReasoningSplit { reasoning, content } = split_reasoning_prefix(
3361            "<think>deduce</think>answer<tool>tail",
3362            Some(&markers),
3363            "<tool>",
3364        );
3365
3366        assert_eq!(reasoning, "deduce");
3367        assert_eq!(content, "answer");
3368    }
3369
3370    #[test]
3371    fn reasoning_markers_from_marker_pair_with_both_present_builds_markers() {
3372        let markers = reasoning_markers_from_marker_pair(
3373            Some("<think>".to_owned()),
3374            Some("</think>".to_owned()),
3375        );
3376
3377        assert_eq!(
3378            markers,
3379            Some(ReasoningMarkers {
3380                open: "<think>".to_owned(),
3381                close: "</think>".to_owned()
3382            })
3383        );
3384    }
3385
3386    #[test]
3387    fn reasoning_markers_from_marker_pair_with_empty_marker_is_none() {
3388        let markers =
3389            reasoning_markers_from_marker_pair(Some(String::new()), Some("</think>".to_owned()));
3390
3391        assert!(markers.is_none());
3392    }
3393
3394    #[test]
3395    fn reasoning_markers_from_marker_pair_with_missing_marker_is_none() {
3396        let markers = reasoning_markers_from_marker_pair(None, Some("</think>".to_owned()));
3397
3398        assert!(markers.is_none());
3399    }
3400
3401    #[test]
3402    fn outcome_from_via_ffi_result_recognized_synthesizes_tool_call_ids() {
3403        let parsed = ParsedChatMessage::new(
3404            "answer".to_owned(),
3405            String::new(),
3406            vec![ParsedToolCall::new(
3407                String::new(),
3408                "tool".to_owned(),
3409                ToolCallArguments::default(),
3410            )],
3411        );
3412
3413        let outcome = outcome_from_via_ffi_result(Ok(parsed), "[]", "answer", false);
3414
3415        assert_eq!(
3416            outcome.unwrap(),
3417            ChatMessageParseOutcome::Recognized(ParsedChatMessage::new(
3418                "answer".to_owned(),
3419                String::new(),
3420                vec![ParsedToolCall::new(
3421                    "call_0".to_owned(),
3422                    "tool".to_owned(),
3423                    ToolCallArguments::default(),
3424                )],
3425            ))
3426        );
3427    }
3428
3429    #[test]
3430    fn outcome_from_via_ffi_result_parse_failed_is_unrecognized_with_raw_message() {
3431        let outcome = outcome_from_via_ffi_result(
3432            Err(ParseChatMessageError::ParseFailed {
3433                message: "boom".to_owned(),
3434            }),
3435            "[]",
3436            "garbled",
3437            true,
3438        );
3439
3440        assert_eq!(
3441            outcome.unwrap(),
3442            ChatMessageParseOutcome::Unrecognized(RawChatMessage {
3443                tools_json: "[]".to_owned(),
3444                text: "garbled".to_owned(),
3445                is_partial: true,
3446                ffi_error_message: "boom".to_owned(),
3447            })
3448        );
3449    }
3450
3451    #[test]
3452    fn outcome_from_via_ffi_result_other_error_propagates() {
3453        let outcome =
3454            outcome_from_via_ffi_result(Err(ParseChatMessageError::NoVocab), "[]", "x", false);
3455
3456        assert_eq!(
3457            discriminant(&outcome.unwrap_err()),
3458            discriminant(&ParseChatMessageError::NoVocab)
3459        );
3460    }
3461}