metrale_bench/benchmarks/vision/
request.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: Building an image chat request, and reading the vision-token
4//! count back out.
5//!
6//! Owner: bench, vision.
7//! Invariants: none beyond the types.
8
9use anyhow::{Context, Result};
10use base64::Engine;
11use serde_json::{Value, json};
12
13/// 2026-09-26: A fixture as a base64 `data:` URI, labelled `image/png`
14/// whatever its bytes; the server picks the decoder from the bytes. The server
15/// fetches `http(s)` image URLs only with `--vision-allow-remote-images`, so a
16/// data URI works on every serve and keeps the network out of the run.
17pub fn data_uri(png: &[u8]) -> String {
18    let mut s = String::from("data:image/png;base64,");
19    base64::engine::general_purpose::STANDARD.encode_string(png, &mut s);
20    s
21}
22
23/// 2026-09-26: One streamed chat request carrying `images`, in order, then
24/// `prompt`.
25///
26/// Temperature 0, since the assertions are about what the model saw. Thinking
27/// is off: a reasoning block can use the whole of `max_tokens` and leave empty
28/// content.
29pub fn body(model: &str, images: &[&[u8]], prompt: &str, max_tokens: usize) -> Value {
30    let mut content: Vec<Value> = images
31        .iter()
32        .map(|png| json!({"type": "image_url", "image_url": {"url": data_uri(png)}}))
33        .collect();
34    content.push(json!({"type": "text", "text": prompt}));
35    json!({
36        "model": model,
37        "stream": true,
38        "temperature": 0.0,
39        "max_tokens": max_tokens,
40        "chat_template_kwargs": {"enable_thinking": false},
41        "messages": [{"role": "user", "content": content}],
42    })
43}
44
45/// 2026-09-26: Vision tokens in a request: `prompt_tokens` minus the template
46/// `overhead`, or an error when `overhead` is larger.
47///
48/// The driver measures `overhead` once per run, from a calibration request
49/// whose vision-token count is known, because it depends on the checkpoint's
50/// chat template.
51pub fn vision_tokens(prompt_tokens: usize, overhead: usize) -> Result<usize> {
52    prompt_tokens.checked_sub(overhead).with_context(|| {
53        format!(
54            "prompt_tokens {prompt_tokens} is below the measured template overhead \
55             {overhead} — the calibration request and this one did not render the same \
56             template, so the subtraction is meaningless"
57        )
58    })
59}
60
61#[cfg(test)]
62#[path = "request_tests.rs"]
63mod request_tests;