1use std::collections::BTreeSet;
2use std::path::Path;
3
4use super::{
5 Embed, Error, FailedCode, ItemCode, Kind, Limits, MINIMUM_FRAME_BYTES, Model, Outcome,
6 PROTOCOL, PROTOCOL_WINDOWS, Ready, Start, Vectors, Window, Windows, decode_vector,
7};
8
9fn limits(message: impl Into<String>) -> Error {
10 Error::new(FailedCode::EmbedLimits, message)
11}
12
13pub fn check_start(start: &Start) -> Result<(), Error> {
19 let windows = match (start.protocol, start.windows) {
21 (PROTOCOL, None) => None,
22 (PROTOCOL_WINDOWS, Some(windows)) => Some(windows),
23 _ => {
24 return Err(Error::new(
25 FailedCode::EmbedProtocol,
26 "unsupported embedding protocol",
27 ));
28 }
29 };
30 let value = &start.limits;
31 if [
32 value.memory_bytes,
33 value.threads,
34 value.batch_items,
35 value.input_bytes,
36 value.tokens,
37 ]
38 .contains(&0)
39 || value.frame_bytes < MINIMUM_FRAME_BYTES
40 {
41 return Err(limits(
42 "all limits must be positive and frame_bytes must be at least 8192",
43 ));
44 }
45 if !Path::new(&start.model_dir).is_absolute() {
46 return Err(limits("model_dir must be absolute"));
47 }
48 if windows.is_some_and(|windows| {
49 windows.max_windows < 2 || windows.overlap_tokens >= start.limits.tokens
50 }) {
51 return Err(limits(
52 "max_windows must be at least 2 and overlap_tokens below tokens",
53 ));
54 }
55 Ok(())
56}
57
58pub fn check_ready(ready: &Ready) -> Result<(), Error> {
64 let model = &ready.model;
65 if ![PROTOCOL, PROTOCOL_WINDOWS].contains(&ready.protocol)
66 || model.name.is_empty()
67 || model.manifest_sha256.len() != 64
68 || !model
69 .manifest_sha256
70 .bytes()
71 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
72 || model.dimensions == 0
73 || model.max_tokens == 0
74 || model.special_tokens >= model.max_tokens
75 {
76 return Err(Error::new(
77 FailedCode::EmbedProtocol,
78 "Ready has invalid protocol or model identity",
79 ));
80 }
81 Ok(())
82}
83
84pub fn vectors_message_size(batch_items: u64, dimensions: u64) -> Result<u64, Error> {
90 if batch_items == 0 || dimensions == 0 {
91 return Err(limits("batch_items and dimensions must be positive"));
92 }
93 let id = 512_u64 * 6;
95 let fixed = u64::try_from(
96 b"{\"outcome\":\"vector\",\"id\":\"\",\"vector\":\"\",\"tokens_read\":,\"tokens_total\":}"
97 .len(),
98 )
99 .map_err(|_| limits("message size overflows"))?;
100 let item = dimensions
101 .checked_mul(8)
102 .and_then(|vector| vector.checked_add(id + fixed + 40))
103 .ok_or_else(|| limits("message size overflows"))?;
104 let items = batch_items
105 .checked_mul(item)
106 .and_then(|value| value.checked_add(batch_items - 1))
107 .ok_or_else(|| limits("message size overflows"))?;
108 items
109 .checked_add(13)
110 .ok_or_else(|| limits("message size overflows"))
111}
112
113pub fn windows_message_size(
119 batch_items: u64,
120 dimensions: u64,
121 max_windows: u64,
122) -> Result<u64, Error> {
123 let overflow = || limits("message size overflows");
124 let fixed = |bytes: &[u8]| u64::try_from(bytes.len()).map_err(|_| overflow());
125 let item = fixed(b"{\"outcome\":\"windows\",\"id\":\"\",\"windows\":[],\"tokens_total\":}")?;
126 let window = fixed(b"{\"start\":,\"end\":,\"vector\":\"\",\"tokens_read\":},")?;
127 let id = 512_u64 * 6;
129 let windows = dimensions
130 .checked_mul(8)
131 .and_then(|vector| vector.checked_add(window + 60))
132 .and_then(|one| one.checked_mul(max_windows))
133 .ok_or_else(overflow)?;
134 let one = windows.checked_add(item + id + 20).ok_or_else(overflow)?;
135 let ordinary = vectors_message_size(batch_items, dimensions)?;
136 batch_items
137 .checked_mul(one)
138 .and_then(|items| items.checked_add(batch_items + 13))
139 .map(|size| size.max(ordinary))
140 .ok_or_else(overflow)
141}
142
143pub fn check_model_limits(limits_value: &Limits, model: &Model) -> Result<(), Error> {
148 check_model_limits_windowed(limits_value, model, None)
149}
150
151pub fn check_model_limits_windowed(
158 limits_value: &Limits,
159 model: &Model,
160 windows: Option<Windows>,
161) -> Result<(), Error> {
162 let minimum_tokens = model
163 .special_tokens
164 .checked_add(model.prefix_tokens)
165 .and_then(|reserved| reserved.checked_add(1));
166 if limits_value.tokens > model.max_tokens
167 || minimum_tokens.is_none_or(|minimum| limits_value.tokens < minimum)
168 {
169 return Err(limits(format!(
170 "tokens {} must be at least special_tokens {} + prefix_tokens {} + 1 and not exceed max_tokens {}",
171 limits_value.tokens, model.special_tokens, model.prefix_tokens, model.max_tokens
172 )));
173 }
174 let largest = match windows {
175 None => vectors_message_size(limits_value.batch_items, model.dimensions)?,
176 Some(windows) => windows_message_size(
177 limits_value.batch_items,
178 model.dimensions,
179 windows.max_windows,
180 )?,
181 };
182 if u64::from(limits_value.frame_bytes) < largest {
183 return Err(limits(
184 "frame_bytes cannot carry the largest legal Vectors batch",
185 ));
186 }
187 let content = model
188 .max_tokens
189 .saturating_sub(model.special_tokens)
190 .saturating_sub(model.prefix_tokens);
191 if windows.is_some_and(|windows| windows.overlap_tokens >= content) {
192 return Err(limits(format!(
193 "overlap_tokens must be below the model's {content} content tokens"
194 )));
195 }
196 Ok(())
197}
198
199fn id_valid(id: &str) -> bool {
200 !id.is_empty() && id.len() <= 512 && !id.contains('\0')
201}
202
203pub fn check_embed(embed: &Embed, limits_value: &Limits) -> Result<(), Error> {
209 if embed.items.is_empty() {
210 return Err(Error::new(
211 FailedCode::EmbedProtocol,
212 "Embed must contain at least one item",
213 ));
214 }
215 let count = u64::try_from(embed.items.len()).map_err(|_| {
216 Error::new(
217 FailedCode::EmbedBatchTooLarge,
218 "batch count exceeds its limit",
219 )
220 })?;
221 if count > limits_value.batch_items {
222 return Err(Error::new(
223 FailedCode::EmbedBatchTooLarge,
224 "Embed exceeds batch_items",
225 ));
226 }
227 let mut seen = BTreeSet::new();
228 for item in &embed.items {
229 if !id_valid(&item.id) || !seen.insert(item.id.as_str()) {
230 return Err(Error::new(
231 FailedCode::EmbedProtocol,
232 "Embed has a bad or repeated id",
233 ));
234 }
235 }
236 Ok(())
237}
238
239pub fn input_refusal(text: &str, input_bytes: u64) -> Option<ItemCode> {
240 if text.chars().all(char::is_whitespace) {
241 return Some(ItemCode::EmbedInputEmpty);
242 }
243 match u64::try_from(text.len()) {
244 Ok(bytes) if bytes <= input_bytes => None,
245 Ok(_) | Err(_) => Some(ItemCode::EmbedInputTooLarge),
246 }
247}
248
249fn invalid_output() -> Error {
250 Error::new(
251 FailedCode::EmbedOutputInvalid,
252 "Vectors content does not satisfy the request and model",
253 )
254}
255
256const fn content_tokens(ready: &Ready) -> u64 {
258 ready
259 .model
260 .max_tokens
261 .saturating_sub(ready.model.special_tokens)
262 .saturating_sub(ready.model.prefix_tokens)
263}
264
265fn refusal_fits(
271 code: ItemCode,
272 expected: Option<ItemCode>,
273 (tokens_total, tokens_limit): (Option<u64>, Option<u64>),
274 ready: &Ready,
275 windowed: bool,
276) -> bool {
277 let longer = |total: u64, limit: u64| {
278 expected.is_none() && limit == ready.model.max_tokens && total > content_tokens(ready)
279 };
280 match (code, tokens_total, tokens_limit) {
281 (ItemCode::TextLongerThanModel, Some(total), Some(limit)) => longer(total, limit),
282 (ItemCode::WindowsOverCap, Some(total), Some(limit)) => windowed && longer(total, limit),
283 (ItemCode::TextLongerThanModel | ItemCode::WindowsOverCap, _, _)
284 | (_, Some(_), _)
285 | (_, _, Some(_)) => false,
286 (_, None, None) => expected == Some(code),
287 }
288}
289
290fn check_vector(vector: &str, ready: &Ready) -> Result<(), Error> {
293 let decoded = decode_vector(vector)?;
294 let dimensions = u64::try_from(decoded.len()).map_err(|_| invalid_output())?;
295 if dimensions != ready.model.dimensions {
296 return Err(invalid_output());
297 }
298 if ready.model.normalised {
299 let squared = decoded
300 .iter()
301 .map(|value| {
302 let member = f64::from(*value);
303 member * member
304 })
305 .sum::<f64>();
306 if (squared.sqrt() - 1.0).abs() > 0.0001 {
307 return Err(invalid_output());
308 }
309 }
310 Ok(())
311}
312
313fn check_windows(
319 windows: &[Window],
320 tokens_total: u64,
321 text: &str,
322 ready: &Ready,
323 limits_value: &Limits,
324 asked: Windows,
325) -> Result<(), Error> {
326 let count = u64::try_from(windows.len()).map_err(|_| invalid_output())?;
327 let length = u64::try_from(text.len()).map_err(|_| invalid_output())?;
328 if count < 2 || count > asked.max_windows || tokens_total <= content_tokens(ready) {
329 return Err(invalid_output());
330 }
331 let boundary = |at: u64| usize::try_from(at).is_ok_and(|at| text.is_char_boundary(at));
332 let mut previous: Option<&Window> = None;
333 for window in windows {
334 let follows = previous.map_or(window.start == 0, |before| {
335 (before.start..=before.end).contains(&window.start)
336 && window.start != before.start
337 && window.end > before.end
338 });
339 if !follows
340 || window.end <= window.start
341 || window.end > length
342 || !boundary(window.start)
343 || !boundary(window.end)
344 || window.tokens_read == 0
345 || window.tokens_read > limits_value.tokens
346 {
347 return Err(invalid_output());
348 }
349 check_vector(&window.vector, ready)?;
350 previous = Some(window);
351 }
352 if previous.map(|last| last.end) != Some(length) {
353 return Err(invalid_output());
354 }
355 Ok(())
356}
357
358pub fn check_vectors(
364 vectors: &Vectors,
365 embed: &Embed,
366 ready: &Ready,
367 limits_value: &Limits,
368) -> Result<(), Error> {
369 check_vectors_windowed(vectors, embed, ready, limits_value, None)
370}
371
372pub fn check_vectors_windowed(
380 vectors: &Vectors,
381 embed: &Embed,
382 ready: &Ready,
383 limits_value: &Limits,
384 windows: Option<Windows>,
385) -> Result<(), Error> {
386 if vectors.items.len() != embed.items.len() {
387 return Err(Error::new(
388 FailedCode::EmbedProtocol,
389 "Vectors count does not match Embed",
390 ));
391 }
392 let windows = windows.filter(|_| embed.kind == Kind::Document);
393 for (outcome, input) in vectors.items.iter().zip(&embed.items) {
394 if outcome.id() != input.id {
395 return Err(Error::new(
396 FailedCode::EmbedProtocol,
397 "Vectors ids do not match Embed order",
398 ));
399 }
400 let expected = input_refusal(&input.text, limits_value.input_bytes);
401 match outcome {
402 Outcome::Refused {
403 code,
404 tokens_total,
405 tokens_limit,
406 ..
407 } => {
408 let counts = (*tokens_total, *tokens_limit);
409 if !refusal_fits(*code, expected, counts, ready, windows.is_some()) {
410 return Err(invalid_output());
411 }
412 }
413 Outcome::Vector {
414 vector,
415 tokens_read,
416 tokens_total,
417 ..
418 } => {
419 if expected.is_some()
420 || *tokens_read == 0
421 || tokens_read > tokens_total
422 || *tokens_read > limits_value.tokens
423 {
424 return Err(invalid_output());
425 }
426 check_vector(vector, ready)?;
427 }
428 Outcome::Windows {
429 windows: made,
430 tokens_total,
431 ..
432 } => {
433 let Some(asked) = windows.filter(|_| expected.is_none()) else {
434 return Err(invalid_output());
435 };
436 check_windows(made, *tokens_total, &input.text, ready, limits_value, asked)?;
437 }
438 }
439 }
440 Ok(())
441}