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