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
87unsafe 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
124unsafe 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
157unsafe 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
234unsafe 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1019unsafe 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
1058unsafe 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
1100unsafe 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
1140unsafe 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
1188unsafe 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
1236unsafe 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
1344unsafe 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
1411unsafe 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
1462unsafe 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
1533unsafe 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 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}