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 return Some(MtmdEvalError::ImageChunkExceedsBatchSize(
72 ImageChunkBatchSizeMismatch {
73 image_tokens: chunk_token_count,
74 n_batch,
75 },
76 ));
77 }
78
79 None
80}
81
82#[derive(Debug)]
83pub struct MtmdInputChunk {
84 pub chunk: NonNull<llama_cpp_bindings_sys::mtmd_input_chunk>,
85 pub owned: bool,
86}
87
88impl MtmdInputChunk {
89 pub fn chunk_type(&self) -> Result<MtmdInputChunkType, MtmdInputChunkTypeError> {
92 let chunk_type =
93 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_type(self.chunk.as_ptr()) };
94 MtmdInputChunkType::try_from(chunk_type)
95 }
96
97 #[must_use]
98 pub fn text_tokens(&self) -> Option<&[LlamaToken]> {
99 if self.chunk_type() != Ok(MtmdInputChunkType::Text) {
100 return None;
101 }
102
103 let mut n_tokens = 0usize;
104 let tokens_ptr = unsafe {
105 llama_cpp_bindings_sys::mtmd_input_chunk_get_tokens_text(
106 self.chunk.as_ptr(),
107 &raw mut n_tokens,
108 )
109 };
110
111 unsafe { tokens_from_raw_ptr(tokens_ptr, n_tokens) }
112 }
113
114 #[must_use]
115 pub fn n_tokens(&self) -> usize {
116 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_n_tokens(self.chunk.as_ptr()) }
117 }
118
119 #[must_use]
120 pub fn n_positions(&self) -> i32 {
121 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_n_pos(self.chunk.as_ptr()) }
122 }
123
124 #[must_use]
125 pub fn id(&self) -> Option<String> {
126 let ptr = unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_get_id(self.chunk.as_ptr()) };
127 if ptr.is_null() {
128 None
129 } else {
130 unsafe { CStr::from_ptr(ptr) }
131 .to_string_lossy()
132 .into_owned()
133 .into()
134 }
135 }
136
137 pub fn copy(&self) -> Result<Self, MtmdInputChunkError> {
141 let chunk = unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_copy(self.chunk.as_ptr()) };
142 let chunk = NonNull::new(chunk).ok_or(MtmdInputChunkError::ChunkOperationFailed)?;
143
144 Ok(Self { chunk, owned: true })
145 }
146
147 pub fn eval_single(
154 &self,
155 mtmd_ctx: &MtmdContext,
156 llama_ctx: &LlamaContext,
157 start_position: llama_cpp_bindings_sys::llama_pos,
158 seq_id: llama_cpp_bindings_sys::llama_seq_id,
159 n_batch: i32,
160 logits_last: bool,
161 ) -> Result<llama_cpp_bindings_sys::llama_pos, MtmdEvalError> {
162 let chunk_token_count = self.n_tokens();
163
164 if let Some(error) = image_chunk_batch_size_error(
165 matches!(self.chunk_type(), Ok(MtmdInputChunkType::Image)),
166 chunk_token_count,
167 n_batch,
168 ) {
169 return Err(error);
170 }
171
172 let mut final_position: llama_cpp_bindings_sys::llama_pos = start_position;
173 let mut out_vendored_return_code: i32 = 0;
174 let mut out_error: *mut c_char = std::ptr::null_mut();
175
176 let status = unsafe {
177 llama_cpp_bindings_sys::llama_rs_mtmd_eval_chunk_single(
178 mtmd_ctx.context.as_ptr(),
179 llama_ctx.context.as_ptr(),
180 self.chunk.as_ptr(),
181 start_position,
182 seq_id,
183 n_batch,
184 logits_last,
185 &raw mut final_position,
186 &raw mut out_vendored_return_code,
187 &raw mut out_error,
188 )
189 };
190
191 eval_chunk_single_status_to_result(
192 status,
193 final_position,
194 out_vendored_return_code,
195 out_error,
196 )
197 }
198}
199
200impl Drop for MtmdInputChunk {
201 fn drop(&mut self) {
202 if self.owned {
203 unsafe { llama_cpp_bindings_sys::mtmd_input_chunk_free(self.chunk.as_ptr()) }
204 }
205 }
206}
207
208#[cfg(test)]
209mod unit_tests {
210 use super::eval_chunk_single_status_to_result;
211 use super::image_chunk_batch_size_error;
212 use super::tokens_from_raw_ptr;
213 use crate::mtmd::image_chunk_batch_size_mismatch::ImageChunkBatchSizeMismatch;
214 use crate::mtmd::mtmd_eval_error::MtmdEvalError;
215
216 #[test]
217 fn tokens_from_raw_ptr_returns_none_for_null() {
218 assert!(unsafe { tokens_from_raw_ptr(std::ptr::null(), 5) }.is_none());
219 }
220
221 #[test]
222 fn tokens_from_raw_ptr_returns_none_for_zero_count() {
223 let token: llama_cpp_bindings_sys::llama_token = 42;
224 assert!(unsafe { tokens_from_raw_ptr(&raw const token, 0) }.is_none());
225 }
226
227 #[test]
228 fn tokens_from_raw_ptr_returns_some_for_valid() {
229 let tokens: [llama_cpp_bindings_sys::llama_token; 2] = [1, 2];
230 let result = unsafe { tokens_from_raw_ptr(tokens.as_ptr(), 2) };
231
232 assert!(result.is_some());
233 assert_eq!(result.unwrap().len(), 2);
234 }
235
236 #[test]
237 fn eval_chunk_single_status_ok_returns_final_position() {
238 let result = eval_chunk_single_status_to_result(
239 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_OK,
240 7,
241 0,
242 std::ptr::null_mut(),
243 );
244
245 assert_eq!(result, Ok(7));
246 }
247
248 #[test]
249 fn eval_chunk_single_status_nonzero_code_maps_to_eval_failed() {
250 let result = eval_chunk_single_status_to_result(
251 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_RETURNED_NONZERO_CODE,
252 0,
253 -3,
254 std::ptr::null_mut(),
255 );
256
257 assert_eq!(result, Err(MtmdEvalError::EvalFailed { code: -3 }));
258 }
259
260 #[test]
261 fn eval_chunk_single_status_allocation_failed_maps_to_not_enough_memory() {
262 let result = eval_chunk_single_status_to_result(
263 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_ERROR_STRING_ALLOCATION_FAILED,
264 0,
265 0,
266 std::ptr::null_mut(),
267 );
268
269 assert_eq!(result, Err(MtmdEvalError::NotEnoughMemory));
270 }
271
272 #[test]
273 fn eval_chunk_single_status_cxx_exception_reports_unknown_error_for_null() {
274 let result = eval_chunk_single_status_to_result(
275 llama_cpp_bindings_sys::LLAMA_RS_MTMD_EVAL_CHUNK_SINGLE_VENDORED_THREW_CXX_EXCEPTION,
276 0,
277 0,
278 std::ptr::null_mut(),
279 );
280
281 assert_eq!(
282 result,
283 Err(MtmdEvalError::Reported {
284 message: "unknown error".to_string()
285 })
286 );
287 }
288
289 #[test]
290 #[should_panic(expected = "llama_rs_mtmd_eval_chunk_single returned unrecognized status")]
291 fn eval_chunk_single_status_unrecognized_panics() {
292 let _ = eval_chunk_single_status_to_result(
293 llama_cpp_bindings_sys::llama_rs_mtmd_eval_chunk_single_status::MAX,
294 0,
295 0,
296 std::ptr::null_mut(),
297 );
298 }
299
300 #[test]
301 fn image_chunk_over_batch_size_reports_mismatch() {
302 let error = image_chunk_batch_size_error(true, 9, 4);
303
304 assert_eq!(
305 error,
306 Some(MtmdEvalError::ImageChunkExceedsBatchSize(
307 ImageChunkBatchSizeMismatch {
308 image_tokens: 9,
309 n_batch: 4,
310 }
311 ))
312 );
313 }
314
315 #[test]
316 fn non_image_chunk_never_reports_mismatch() {
317 assert!(image_chunk_batch_size_error(false, 9, 4).is_none());
318 }
319
320 #[test]
321 fn image_chunk_within_batch_size_reports_no_mismatch() {
322 assert!(image_chunk_batch_size_error(true, 4, 4).is_none());
323 }
324}