1use std::ffi::CStr;
2use std::ffi::c_char;
3use std::ptr::NonNull;
4use std::slice;
5
6use crate::context::LlamaContext;
7use crate::ffi_error_reader::read_and_free_cpp_error;
8use crate::token::LlamaToken;
9
10use super::image_chunk_batch_size_mismatch::ImageChunkBatchSizeMismatch;
11use super::mtmd_context::MtmdContext;
12use super::mtmd_eval_error::MtmdEvalError;
13use super::mtmd_input_chunk_error::MtmdInputChunkError;
14use super::mtmd_input_chunk_type::MtmdInputChunkType;
15use super::mtmd_input_chunk_type_error::MtmdInputChunkTypeError;
16
17const unsafe fn tokens_from_raw_ptr<'chunk>(
22 tokens_ptr: *const llama_cpp_bindings_sys::llama_token,
23 n_tokens: usize,
24) -> Option<&'chunk [LlamaToken]> {
25 if tokens_ptr.is_null() || n_tokens == 0 {
26 None
27 } else {
28 unsafe {
29 Some(slice::from_raw_parts(
30 tokens_ptr.cast::<LlamaToken>(),
31 n_tokens,
32 ))
33 }
34 }
35}
36
37fn eval_chunk_single_status_to_result(
38 status: llama_cpp_bindings_sys::llama_rs_mtmd_eval_chunk_single_status,
39 final_position: llama_cpp_bindings_sys::llama_pos,
40 out_vendored_return_code: i32,
41 out_error: *mut c_char,
42) -> Result<llama_cpp_bindings_sys::llama_pos, MtmdEvalError> {
43 match status {
44 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_OK => Ok(final_position),
45 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_RETURNED_NONZERO_CODE => {
46 Err(MtmdEvalError::EvalFailed {
47 code: out_vendored_return_code,
48 })
49 }
50 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_ERROR_STRING_ALLOCATION_FAILED => {
51 Err(MtmdEvalError::NotEnoughMemory)
52 }
53 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_THREW_CXX_EXCEPTION => {
54 let message = unsafe { read_and_free_cpp_error(out_error) };
55 Err(MtmdEvalError::Reported { message })
56 }
57 other => {
58 unreachable!("llama_rs_mtmd_eval_chunk_single returned unrecognized status: {other}")
59 }
60 }
61}
62
63fn image_chunk_batch_size_error(
64 is_image_chunk: bool,
65 chunk_token_count: usize,
66 n_batch: i32,
67) -> Option<MtmdEvalError> {
68 if is_image_chunk
69 && i64::try_from(chunk_token_count).is_ok_and(|tokens| tokens > i64::from(n_batch))
70 {
71 #[expect(
72 clippy::cast_possible_truncation,
73 clippy::cast_sign_loss,
74 reason = "image token counts and n_batch are model-bounded and fit in u32"
75 )]
76 return Some(MtmdEvalError::ImageChunkExceedsBatchSize(
77 ImageChunkBatchSizeMismatch {
78 image_tokens: chunk_token_count as u32,
79 n_batch: n_batch as u32,
80 },
81 ));
82 }
83
84 None
85}
86
87#[derive(Debug)]
88pub struct MtmdInputChunk {
89 pub chunk: NonNull<llama_cpp_bindings_sys::mtmd_input_chunk>,
90 pub owned: bool,
91}
92
93impl MtmdInputChunk {
94 pub fn chunk_type(&self) -> Result<MtmdInputChunkType, MtmdInputChunkTypeError> {
97 let chunk_type =
98 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_type(self.chunk.as_ptr()) };
99 MtmdInputChunkType::try_from(chunk_type)
100 }
101
102 #[must_use]
103 pub fn text_tokens(&self) -> Option<&[LlamaToken]> {
104 if self.chunk_type() != Ok(MtmdInputChunkType::Text) {
105 return None;
106 }
107
108 let mut n_tokens = 0usize;
109 let tokens_ptr = unsafe {
110 llama_cpp_bindings_sys::mtmd_input_chunk_get_tokens_text(
111 self.chunk.as_ptr(),
112 &raw mut n_tokens,
113 )
114 };
115
116 unsafe { tokens_from_raw_ptr(tokens_ptr, n_tokens) }
117 }
118
119 #[must_use]
120 pub fn n_tokens(&self) -> usize {
121 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_n_tokens(self.chunk.as_ptr()) }
122 }
123
124 #[must_use]
125 pub fn n_positions(&self) -> i32 {
126 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_n_pos(self.chunk.as_ptr()) }
127 }
128
129 #[must_use]
130 pub fn id(&self) -> Option<String> {
131 let ptr = unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_id(self.chunk.as_ptr()) };
132 if ptr.is_null() {
133 None
134 } else {
135 unsafe { CStr::from_ptr(ptr) }
136 .to_string_lossy()
137 .into_owned()
138 .into()
139 }
140 }
141
142 pub fn copy(&self) -> Result<Self, MtmdInputChunkError> {
146 let chunk = unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_copy(self.chunk.as_ptr()) };
147 let chunk = NonNull::new(chunk).ok_or(MtmdInputChunkError::ChunkOperationFailed)?;
148
149 Ok(Self { chunk, owned: true })
150 }
151
152 pub fn eval_single(
159 &self,
160 mtmd_ctx: &MtmdContext,
161 llama_ctx: &LlamaContext,
162 start_position: llama_cpp_bindings_sys::llama_pos,
163 seq_id: llama_cpp_bindings_sys::llama_seq_id,
164 n_batch: i32,
165 logits_last: bool,
166 ) -> Result<llama_cpp_bindings_sys::llama_pos, MtmdEvalError> {
167 let chunk_token_count = self.n_tokens();
168
169 if let Some(error) = image_chunk_batch_size_error(
170 matches!(self.chunk_type(), Ok(MtmdInputChunkType::Image)),
171 chunk_token_count,
172 n_batch,
173 ) {
174 return Err(error);
175 }
176
177 let mut final_position: llama_cpp_bindings_sys::llama_pos = start_position;
178 let mut out_vendored_return_code: i32 = 0;
179 let mut out_error: *mut c_char = std::ptr::null_mut();
180
181 let status = unsafe {
182 llama_cpp_bindings_sys::llama_rs_mtmd_eval_chunk_single(
183 mtmd_ctx.context.as_ptr(),
184 llama_ctx.context.as_ptr(),
185 self.chunk.as_ptr(),
186 start_position,
187 seq_id,
188 n_batch,
189 logits_last,
190 &raw mut final_position,
191 &raw mut out_vendored_return_code,
192 &raw mut out_error,
193 )
194 };
195
196 eval_chunk_single_status_to_result(
197 status,
198 final_position,
199 out_vendored_return_code,
200 out_error,
201 )
202 }
203}
204
205impl Drop for MtmdInputChunk {
206 fn drop(&mut self) {
207 if self.owned {
208 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_free(self.chunk.as_ptr()) }
209 }
210 }
211}
212
213#[cfg(test)]
214mod unit_tests {
215 use super::eval_chunk_single_status_to_result;
216 use super::image_chunk_batch_size_error;
217 use super::tokens_from_raw_ptr;
218 use crate::mtmd::image_chunk_batch_size_mismatch::ImageChunkBatchSizeMismatch;
219 use crate::mtmd::mtmd_eval_error::MtmdEvalError;
220
221 #[test]
222 fn tokens_from_raw_ptr_returns_none_for_null() {
223 assert!(unsafe { tokens_from_raw_ptr(std::ptr::null(), 5) }.is_none());
224 }
225
226 #[test]
227 fn tokens_from_raw_ptr_returns_none_for_zero_count() {
228 let token: llama_cpp_bindings_sys::llama_token = 42;
229 assert!(unsafe { tokens_from_raw_ptr(&raw const token, 0) }.is_none());
230 }
231
232 #[test]
233 fn tokens_from_raw_ptr_returns_some_for_valid() {
234 let tokens: [llama_cpp_bindings_sys::llama_token; 2] = [1, 2];
235 let result = unsafe { tokens_from_raw_ptr(tokens.as_ptr(), 2) };
236
237 assert!(result.is_some());
238 assert_eq!(result.unwrap().len(), 2);
239 }
240
241 #[test]
242 fn eval_chunk_single_status_ok_returns_final_position() {
243 let result = eval_chunk_single_status_to_result(
244 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_OK,
245 7,
246 0,
247 std::ptr::null_mut(),
248 );
249
250 assert_eq!(result, Ok(7));
251 }
252
253 #[test]
254 fn eval_chunk_single_status_nonzero_code_maps_to_eval_failed() {
255 let result = eval_chunk_single_status_to_result(
256 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_RETURNED_NONZERO_CODE,
257 0,
258 -3,
259 std::ptr::null_mut(),
260 );
261
262 assert_eq!(result, Err(MtmdEvalError::EvalFailed { code: -3 }));
263 }
264
265 #[test]
266 fn eval_chunk_single_status_allocation_failed_maps_to_not_enough_memory() {
267 let result = eval_chunk_single_status_to_result(
268 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_ERROR_STRING_ALLOCATION_FAILED,
269 0,
270 0,
271 std::ptr::null_mut(),
272 );
273
274 assert_eq!(result, Err(MtmdEvalError::NotEnoughMemory));
275 }
276
277 #[test]
278 fn eval_chunk_single_status_cxx_exception_reports_unknown_error_for_null() {
279 let result = eval_chunk_single_status_to_result(
280 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_THREW_CXX_EXCEPTION,
281 0,
282 0,
283 std::ptr::null_mut(),
284 );
285
286 assert_eq!(
287 result,
288 Err(MtmdEvalError::Reported {
289 message: "unknown error".to_string()
290 })
291 );
292 }
293
294 #[test]
295 #[should_panic(expected = "llama_rs_mtmd_eval_chunk_single returned unrecognized status")]
296 fn eval_chunk_single_status_unrecognized_panics() {
297 let _ = eval_chunk_single_status_to_result(
298 llama_cpp_bindings_sys::llama_rs_mtmd_eval_chunk_single_status::MAX,
299 0,
300 0,
301 std::ptr::null_mut(),
302 );
303 }
304
305 #[test]
306 fn image_chunk_over_batch_size_reports_mismatch() {
307 let error = image_chunk_batch_size_error(true, 9, 4);
308
309 assert_eq!(
310 error,
311 Some(MtmdEvalError::ImageChunkExceedsBatchSize(
312 ImageChunkBatchSizeMismatch {
313 image_tokens: 9,
314 n_batch: 4,
315 }
316 ))
317 );
318 }
319
320 #[test]
321 fn non_image_chunk_never_reports_mismatch() {
322 assert!(image_chunk_batch_size_error(false, 9, 4).is_none());
323 }
324
325 #[test]
326 fn image_chunk_within_batch_size_reports_no_mismatch() {
327 assert!(image_chunk_batch_size_error(true, 4, 4).is_none());
328 }
329}