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> {
14 let windows = match (start.protocol, start.windows) {
16 (PROTOCOL, None) => None,
17 (PROTOCOL_WINDOWS, Some(windows)) => Some(windows),
18 _ => {
19 return Err(Error::new(
20 FailedCode::EmbedProtocol,
21 "unsupported embedding protocol",
22 ));
23 }
24 };
25 let value = &start.limits;
26 if [
27 value.memory_bytes,
28 value.threads,
29 value.batch_items,
30 value.input_bytes,
31 value.tokens,
32 ]
33 .contains(&0)
34 || value.frame_bytes < MINIMUM_FRAME_BYTES
35 {
36 return Err(limits(
37 "all limits must be positive and frame_bytes must be at least 8192",
38 ));
39 }
40 if !Path::new(&start.model_dir).is_absolute() {
41 return Err(limits("model_dir must be absolute"));
42 }
43 if windows.is_some_and(|windows| {
44 windows.max_windows < 2 || windows.overlap_tokens >= start.limits.tokens
45 }) {
46 return Err(limits(
47 "max_windows must be at least 2 and overlap_tokens below tokens",
48 ));
49 }
50 Ok(())
51}
52
53pub fn check_ready(ready: &Ready) -> Result<(), Error> {
56 let model = &ready.model;
57 if ![PROTOCOL, PROTOCOL_WINDOWS].contains(&ready.protocol)
58 || model.name.is_empty()
59 || model.manifest_sha256.len() != 64
60 || !model
61 .manifest_sha256
62 .bytes()
63 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
64 || model.dimensions == 0
65 || model.max_tokens == 0
66 || model.special_tokens >= model.max_tokens
67 {
68 return Err(Error::new(
69 FailedCode::EmbedProtocol,
70 "Ready has invalid protocol or model identity",
71 ));
72 }
73 Ok(())
74}
75
76pub fn vectors_message_size(batch_items: u64, dimensions: u64) -> Result<u64, Error> {
79 if batch_items == 0 || dimensions == 0 {
80 return Err(limits("batch_items and dimensions must be positive"));
81 }
82 let id = 512_u64 * 6;
84 let fixed = u64::try_from(
85 b"{\"outcome\":\"vector\",\"id\":\"\",\"vector\":\"\",\"tokens_read\":,\"tokens_total\":}"
86 .len(),
87 )
88 .map_err(|_| limits("message size overflows"))?;
89 let item = dimensions
90 .checked_mul(8)
91 .and_then(|vector| vector.checked_add(id + fixed + 40))
92 .ok_or_else(|| limits("message size overflows"))?;
93 let items = batch_items
94 .checked_mul(item)
95 .and_then(|value| value.checked_add(batch_items - 1))
96 .ok_or_else(|| limits("message size overflows"))?;
97 items
98 .checked_add(13)
99 .ok_or_else(|| limits("message size overflows"))
100}
101
102pub fn windows_message_size(
108 batch_items: u64,
109 dimensions: u64,
110 max_windows: u64,
111) -> Result<u64, Error> {
112 let overflow = || limits("message size overflows");
113 let fixed = |bytes: &[u8]| u64::try_from(bytes.len()).map_err(|_| overflow());
114 let item = fixed(b"{\"outcome\":\"windows\",\"id\":\"\",\"windows\":[],\"tokens_total\":}")?;
115 let window = fixed(b"{\"start\":,\"end\":,\"vector\":\"\",\"tokens_read\":},")?;
116 let id = 512_u64 * 6;
118 let windows = dimensions
119 .checked_mul(8)
120 .and_then(|vector| vector.checked_add(window + 60))
121 .and_then(|one| one.checked_mul(max_windows))
122 .ok_or_else(overflow)?;
123 let one = windows.checked_add(item + id + 20).ok_or_else(overflow)?;
124 let ordinary = vectors_message_size(batch_items, dimensions)?;
125 batch_items
126 .checked_mul(one)
127 .and_then(|items| items.checked_add(batch_items + 13))
128 .map(|size| size.max(ordinary))
129 .ok_or_else(overflow)
130}
131
132pub fn check_model_limits(limits_value: &Limits, model: &Model) -> Result<(), Error> {
133 check_model_limits_windowed(limits_value, model, None)
134}
135
136pub fn check_model_limits_windowed(
143 limits_value: &Limits,
144 model: &Model,
145 windows: Option<Windows>,
146) -> Result<(), Error> {
147 let minimum_tokens = model
148 .special_tokens
149 .checked_add(model.prefix_tokens)
150 .and_then(|reserved| reserved.checked_add(1));
151 if limits_value.tokens > model.max_tokens
152 || minimum_tokens.is_none_or(|minimum| limits_value.tokens < minimum)
153 {
154 return Err(limits(format!(
155 "tokens {} must be at least special_tokens {} + prefix_tokens {} + 1 and not exceed max_tokens {}",
156 limits_value.tokens, model.special_tokens, model.prefix_tokens, model.max_tokens
157 )));
158 }
159 let largest = match windows {
160 None => vectors_message_size(limits_value.batch_items, model.dimensions)?,
161 Some(windows) => windows_message_size(
162 limits_value.batch_items,
163 model.dimensions,
164 windows.max_windows,
165 )?,
166 };
167 if u64::from(limits_value.frame_bytes) < largest {
168 return Err(limits(
169 "frame_bytes cannot carry the largest legal Vectors batch",
170 ));
171 }
172 let content = model
173 .max_tokens
174 .saturating_sub(model.special_tokens)
175 .saturating_sub(model.prefix_tokens);
176 if windows.is_some_and(|windows| windows.overlap_tokens >= content) {
177 return Err(limits(format!(
178 "overlap_tokens must be below the model's {content} content tokens"
179 )));
180 }
181 Ok(())
182}
183
184fn id_valid(id: &str) -> bool {
185 !id.is_empty() && id.len() <= 512 && !id.contains('\0')
186}
187
188pub fn check_embed(embed: &Embed, limits_value: &Limits) -> Result<(), Error> {
189 if embed.items.is_empty() {
190 return Err(Error::new(
191 FailedCode::EmbedProtocol,
192 "Embed must contain at least one item",
193 ));
194 }
195 let count = u64::try_from(embed.items.len()).map_err(|_| {
196 Error::new(
197 FailedCode::EmbedBatchTooLarge,
198 "batch count exceeds its limit",
199 )
200 })?;
201 if count > limits_value.batch_items {
202 return Err(Error::new(
203 FailedCode::EmbedBatchTooLarge,
204 "Embed exceeds batch_items",
205 ));
206 }
207 let mut seen = BTreeSet::new();
208 for item in &embed.items {
209 if !id_valid(&item.id) || !seen.insert(item.id.as_str()) {
210 return Err(Error::new(
211 FailedCode::EmbedProtocol,
212 "Embed has a bad or repeated id",
213 ));
214 }
215 }
216 Ok(())
217}
218
219pub fn input_refusal(text: &str, input_bytes: u64) -> Option<ItemCode> {
220 if text.chars().all(char::is_whitespace) {
221 return Some(ItemCode::EmbedInputEmpty);
222 }
223 match u64::try_from(text.len()) {
224 Ok(bytes) if bytes <= input_bytes => None,
225 Ok(_) | Err(_) => Some(ItemCode::EmbedInputTooLarge),
226 }
227}
228
229fn invalid_output() -> Error {
230 Error::new(
231 FailedCode::EmbedOutputInvalid,
232 "Vectors content does not satisfy the request and model",
233 )
234}
235
236const fn content_tokens(ready: &Ready) -> u64 {
238 ready
239 .model
240 .max_tokens
241 .saturating_sub(ready.model.special_tokens)
242 .saturating_sub(ready.model.prefix_tokens)
243}
244
245fn refusal_fits(
251 code: ItemCode,
252 expected: Option<ItemCode>,
253 (tokens_total, tokens_limit): (Option<u64>, Option<u64>),
254 ready: &Ready,
255 windowed: bool,
256) -> bool {
257 let longer = |total: u64, limit: u64| {
258 expected.is_none() && limit == ready.model.max_tokens && total > content_tokens(ready)
259 };
260 match (code, tokens_total, tokens_limit) {
261 (ItemCode::TextLongerThanModel, Some(total), Some(limit)) => longer(total, limit),
262 (ItemCode::WindowsOverCap, Some(total), Some(limit)) => windowed && longer(total, limit),
263 (ItemCode::TextLongerThanModel | ItemCode::WindowsOverCap, _, _)
264 | (_, Some(_), _)
265 | (_, _, Some(_)) => false,
266 (_, None, None) => expected == Some(code),
267 }
268}
269
270fn check_vector(vector: &str, ready: &Ready) -> Result<(), Error> {
273 let decoded = decode_vector(vector)?;
274 let dimensions = u64::try_from(decoded.len()).map_err(|_| invalid_output())?;
275 if dimensions != ready.model.dimensions {
276 return Err(invalid_output());
277 }
278 if ready.model.normalised {
279 let squared = decoded
280 .iter()
281 .map(|value| {
282 let member = f64::from(*value);
283 member * member
284 })
285 .sum::<f64>();
286 if (squared.sqrt() - 1.0).abs() > 0.0001 {
287 return Err(invalid_output());
288 }
289 }
290 Ok(())
291}
292
293fn check_windows(
299 windows: &[Window],
300 tokens_total: u64,
301 text: &str,
302 ready: &Ready,
303 limits_value: &Limits,
304 asked: Windows,
305) -> Result<(), Error> {
306 let count = u64::try_from(windows.len()).map_err(|_| invalid_output())?;
307 let length = u64::try_from(text.len()).map_err(|_| invalid_output())?;
308 if count < 2 || count > asked.max_windows || tokens_total <= content_tokens(ready) {
309 return Err(invalid_output());
310 }
311 let boundary = |at: u64| usize::try_from(at).is_ok_and(|at| text.is_char_boundary(at));
312 let mut previous: Option<&Window> = None;
313 for window in windows {
314 let follows = previous.map_or(window.start == 0, |before| {
315 (before.start..=before.end).contains(&window.start)
316 && window.start != before.start
317 && window.end > before.end
318 });
319 if !follows
320 || window.end <= window.start
321 || window.end > length
322 || !boundary(window.start)
323 || !boundary(window.end)
324 || window.tokens_read == 0
325 || window.tokens_read > limits_value.tokens
326 {
327 return Err(invalid_output());
328 }
329 check_vector(&window.vector, ready)?;
330 previous = Some(window);
331 }
332 if previous.map(|last| last.end) != Some(length) {
333 return Err(invalid_output());
334 }
335 Ok(())
336}
337
338pub fn check_vectors(
339 vectors: &Vectors,
340 embed: &Embed,
341 ready: &Ready,
342 limits_value: &Limits,
343) -> Result<(), Error> {
344 check_vectors_windowed(vectors, embed, ready, limits_value, None)
345}
346
347pub fn check_vectors_windowed(
355 vectors: &Vectors,
356 embed: &Embed,
357 ready: &Ready,
358 limits_value: &Limits,
359 windows: Option<Windows>,
360) -> Result<(), Error> {
361 if vectors.items.len() != embed.items.len() {
362 return Err(Error::new(
363 FailedCode::EmbedProtocol,
364 "Vectors count does not match Embed",
365 ));
366 }
367 let windows = windows.filter(|_| embed.kind == Kind::Document);
368 for (outcome, input) in vectors.items.iter().zip(&embed.items) {
369 if outcome.id() != input.id {
370 return Err(Error::new(
371 FailedCode::EmbedProtocol,
372 "Vectors ids do not match Embed order",
373 ));
374 }
375 let expected = input_refusal(&input.text, limits_value.input_bytes);
376 match outcome {
377 Outcome::Refused {
378 code,
379 tokens_total,
380 tokens_limit,
381 ..
382 } => {
383 let counts = (*tokens_total, *tokens_limit);
384 if !refusal_fits(*code, expected, counts, ready, windows.is_some()) {
385 return Err(invalid_output());
386 }
387 }
388 Outcome::Vector {
389 vector,
390 tokens_read,
391 tokens_total,
392 ..
393 } => {
394 if expected.is_some()
395 || *tokens_read == 0
396 || tokens_read > tokens_total
397 || *tokens_read > limits_value.tokens
398 {
399 return Err(invalid_output());
400 }
401 check_vector(vector, ready)?;
402 }
403 Outcome::Windows {
404 windows: made,
405 tokens_total,
406 ..
407 } => {
408 let Some(asked) = windows.filter(|_| expected.is_none()) else {
409 return Err(invalid_output());
410 };
411 check_windows(made, *tokens_total, &input.text, ready, limits_value, asked)?;
412 }
413 }
414 }
415 Ok(())
416}