Skip to main content

haem_frames/embedding/
checks.rs

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
13/// Validate the protocol, model path and positive exchange limits.
14///
15/// # Errors
16/// Returns `embed_protocol` for an unsupported protocol or windows shape, and
17/// `embed_limits` for invalid limits or a relative model path.
18pub fn check_start(start: &Start) -> Result<(), Error> {
19    // EMB-E2: windows are asked for by protocol 2 and only by it.
20    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
58/// Ready names protocol 1 or, for a Start that asked for windows, 2; the
59/// service checks it names the protocol its Start did.
60///
61/// # Errors
62/// Returns `embed_protocol` for an invalid protocol or model identity.
63pub 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
84/// Upper bound for tag plus compact JSON, not the usual message size.
85/// Includes maximally escaped ids and the widest whole-number token counts.
86///
87/// # Errors
88/// Returns `embed_limits` for zero counts or an overflowing message size.
89pub 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    // Each legal single-byte control character can take six JSON bytes.
94    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
113/// EMB-E2: [`vectors_message_size`] when each item may answer up to
114/// `max_windows` windows, each its bounds, its vector and its token count.
115///
116/// # Errors
117/// `embed_limits` when a count is zero or the size overflows.
118pub 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    // Each legal single-byte control character can take six JSON bytes.
128    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
143/// Check that token and frame limits fit the model and its largest answer.
144///
145/// # Errors
146/// Returns `embed_limits`, naming the incompatible limit or size overflow.
147pub fn check_model_limits(limits_value: &Limits, model: &Model) -> Result<(), Error> {
148    check_model_limits_windowed(limits_value, model, None)
149}
150
151/// [`check_model_limits`], and with `windows` (EMB-E2) a frame that carries
152/// the largest windowed answer and an overlap below the model's content
153/// tokens, so every window moves forward through its text.
154///
155/// # Errors
156/// `embed_limits`, naming the limit.
157pub 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
203/// Check a batch has a bounded item count and distinct valid identifiers.
204///
205/// # Errors
206/// Returns `embed_protocol` for an empty batch or invalid ids, and
207/// `embed_batch_too_large` when the item count exceeds the limit.
208pub 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
256/// The content tokens the model reads after its special and prefix tokens.
257const 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
265/// A refusal is valid when it names the byte classifier's own code and carries no token counts,
266/// or, for a text the classifier passed, says `text_longer_than_model` with the text's content
267/// tokens beyond what the model reads after its special and prefix tokens, and the model's limit
268/// as Ready declared it. EMB-E2: `windows_over_cap` is the same, and only when windows were
269/// asked for.
270fn 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
290/// One encoded vector of the model's dimensions, of length one when the
291/// model says its vectors are normalised.
292fn 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
313/// EMB-E2: a text's windows cover it whole. The first starts at 0 and the
314/// last ends at its length; each starts after the one before it starts, no
315/// later than it ends, and ends after it; every bound is on a char boundary;
316/// there are 2 to `max_windows` of them, for a text longer than the model
317/// reads; and each window was read whole.
318fn 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
358/// Check answer count, order, vectors and refusal outcomes against the request.
359///
360/// # Errors
361/// Returns `embed_protocol` for a count or order mismatch, and
362/// `embed_output_invalid` for outcomes the request and model do not allow.
363pub 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
372/// [`check_vectors`] for a worker started with `windows` (EMB-E2): an item of
373/// a document Embed may also answer its windows, or `windows_over_cap`; a
374/// question is never windowed.
375///
376/// # Errors
377/// `embed_protocol` for a count or order that does not match the Embed;
378/// `embed_output_invalid` for any outcome the request and model do not allow.
379pub 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}