1use crate::hardware::Sensitivity;
13use std::time::{Duration, Instant};
14
15use anyhow::{Context, Result, bail};
16
17use crate::benchmark::{Benchmark, BenchmarkDescriptor};
18use crate::benchmarks::one_line;
19use crate::http;
20use crate::metadata::PluginMetadata;
21use crate::params::{ParamKind, ParamSpec, ParamValue, ParamValues};
22use crate::plugin::{Plugin, PluginHandle};
23use crate::result::{BenchmarkResult, LogLine, RunStatus, Stat, Verdict as RunVerdict};
24
25use super::geometry::expected_vision_tokens_bounded;
26use super::probes::{CONTROL, PROBES, Probe, concurrency_probe};
27use super::provision::{self, FIXTURES, provision};
28use super::request;
29use super::score::{
30 GeomCell, ProbeCell, Verdict as VisionVerdict, asserted_cells, reply_matches, verdict,
31 with_runtime_checks,
32};
33
34const SUMMARY: &str = "Vision fidelity: exact vision-token geometry across a resolution ladder, \
35 plus capability probes with a no-image control.";
36
37pub const METADATA: PluginMetadata = PluginMetadata::metrale(SUMMARY);
38
39pub const DESCRIPTOR: BenchmarkDescriptor = BenchmarkDescriptor {
40 id: "vision-fidelity",
41 name: "Vision Fidelity",
42 summary: SUMMARY,
43 detail: "Two legs. GEOMETRY sends a ladder of committed fixtures (224² through 1280×720, \
44 square, wide and portrait, deliberately mixing grid-exact sizes with ones that \
45 must snap) and asserts the EXACT vision-token count from usage.prompt_tokens \
46 against patch/merge arithmetic — the observable that moves when preprocessing \
47 changes, and the one a capability check cannot see. CAPABILITY asks unambiguous \
48 questions about those images. A no-image CONTROL runs last: if it answers as \
49 though it saw a picture, the capability leg proved nothing and the run reports \
50 VACUOUS rather than PASS. Images the encoder REFUSES report UNMEASURED, never \
51 FAIL — that is a deployment setting, not a defect. A serve that caps vision \
52 area does not refuse, it downscales: declare its bound with vision_max_pixels \
53 and the ladder predicts the downscaled geometry and still asserts every rung.",
54 duration_hint: "~1-2 min",
55 expected_secs: 120,
56 updated: "2026-08-14",
57 needs_confirmation: false,
58 intended_for: None,
61 threshold_params: &[],
62 sensitivity: Sensitivity::Correctness,
65 ctor: || Box::new(VisionFidelity::default()),
66};
67
68#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
69enum Phase {
70 #[default]
71 Calibrate,
72 Geometry,
73 Probes,
74 Integrity,
75 Concurrency,
76 Control,
77 Score,
78 Done,
79}
80
81#[derive(Default)]
82pub struct VisionFidelity {
83 handle: Option<PluginHandle>,
84 phase: Phase,
85 started: Option<Instant>,
86 overhead: Option<usize>,
89 geom: Vec<GeomCell>,
90 probes: Vec<ProbeCell>,
91 control_held: bool,
92 cursor: usize,
93 conc_results: Vec<crate::benchmarks::video::concurrency::LevelResult>,
94 integrity: Vec<crate::benchmarks::media_integrity::Cell>,
95 max_tokens: usize,
96 request_timeout_s: u64,
97 vision_max_pixels: u64,
101}
102
103impl VisionFidelity {
104 fn handle(&self) -> Result<&PluginHandle> {
105 self.handle.as_ref().context("benchmark was not loaded")
106 }
107
108 fn elapsed(&self) -> Duration {
109 self.started.map(|s| s.elapsed()).unwrap_or_default()
110 }
111
112 fn timeout(&self) -> Duration {
113 Duration::from_secs(self.request_timeout_s)
114 }
115
116 fn fixture(&self, name: &str) -> Result<&'static [u8]> {
117 FIXTURES
118 .iter()
119 .find(|(n, _, _, _)| *n == name)
120 .map(|(_, b, _, _)| *b)
121 .with_context(|| format!("fixture {name} is not in the provisioned set"))
122 }
123
124 fn frame(&self, phase: &str, log: Vec<LogLine>) -> BenchmarkResult {
125 let mut r = BenchmarkResult::running(phase, self.elapsed());
126 r.progress = Some((
127 (self.geom.len() + self.probes.len()) as u64,
128 (FIXTURES.len() + PROBES.len()) as u64,
129 ));
130 r.log = log;
131 r
132 }
133
134 async fn run_probe(&self, p: &Probe) -> ProbeCell {
136 let handle = match self.handle() {
137 Ok(h) => h,
138 Err(e) => {
139 return ProbeCell::Error {
140 id: p.id,
141 msg: one_line(format!("{e:#}")),
142 };
143 }
144 };
145 let images: Vec<&[u8]> = match p
146 .images
147 .iter()
148 .map(|n| self.fixture(n))
149 .collect::<Result<Vec<_>>>()
150 {
151 Ok(v) => v,
152 Err(e) => {
153 return ProbeCell::Error {
154 id: p.id,
155 msg: one_line(format!("{e:#}")),
156 };
157 }
158 };
159 let body = request::body(&handle.target().model, &images, p.prompt, self.max_tokens);
160 match http::chat_stream(handle.target(), &body, self.timeout()).await {
161 Ok(o) => {
162 if reply_matches(&o.text, p.want_all, p.want_none) {
163 ProbeCell::Pass { id: p.id }
164 } else {
165 ProbeCell::Fail {
166 id: p.id,
167 reply: one_line(&o.text),
168 }
169 }
170 }
171 Err(e) => ProbeCell::Error {
172 id: p.id,
173 msg: one_line(format!("{e:#}")),
174 },
175 }
176 }
177}
178
179impl Plugin for VisionFidelity {
180 fn metadata(&self) -> &'static PluginMetadata {
181 &METADATA
182 }
183
184 async fn load(&mut self, handle: PluginHandle) -> Result<()> {
185 provision(handle.artifacts()).context("provisioning vision fixtures")?;
188 self.handle = Some(handle);
189 Ok(())
190 }
191}
192
193impl Benchmark for VisionFidelity {
194 fn descriptor(&self) -> &'static BenchmarkDescriptor {
195 &DESCRIPTOR
196 }
197
198 fn parameters(&self) -> Vec<ParamSpec> {
199 vec![
200 ParamSpec::new(
201 "max_tokens",
202 "Max tokens per reply",
203 "Probe replies are short by design; the geometry leg needs almost none. \
204 Keep this well above the model's thinking budget if you disable the \
205 thinking-off default, or a reasoning block will consume the whole budget \
206 and return empty content that reads as a vision failure.",
207 ParamKind::Int { min: 16, max: 2048 },
208 ParamValue::Int(128),
209 ),
210 ParamSpec::new(
211 "request_timeout_s",
212 "Per-request timeout (s)",
213 "A large fixture at a high area bound can take a while to prefill.",
214 ParamKind::Int { min: 30, max: 3600 },
215 ParamValue::Int(300),
216 ),
217 ParamSpec::new(
218 "vision_max_pixels",
219 "Target serve's --vision-max-pixels (0 = not set)",
220 "Set this to the SAME area the target server was started with. It is an \
221 area in pixels, not an edge length, exactly like the flag. Leave it 0 \
222 when the serve does not pass the flag — the checkpoint's own bound is \
223 far above every fixture, so nothing downscales and the ladder asserts \
224 native geometry. \
225 \n\nWhy this exists: a serve that caps vision area does NOT refuse an \
226 oversized image, it silently downscales it and answers normally. The \
227 reply then carries fewer vision tokens than a native-resolution \
228 prediction expects, and the cell fails while the model is perfectly \
229 healthy. A run against `--vision-max-pixels 262144` scored 9/14 for \
230 exactly that reason (2026-08-21) — the five fixtures above the bound. \
231 Declaring the bound here does not SKIP those rungs: the prediction is \
232 recomputed for the downscaled geometry and the rung is still asserted, \
233 so an engine that does not honour its own declared bound still fails.",
234 ParamKind::Int {
235 min: 0,
236 max: 16_777_216,
239 },
240 ParamValue::Int(0),
241 ),
242 ]
243 }
244
245 fn configure(&mut self, values: &ParamValues) -> Result<()> {
246 self.max_tokens = values.int("max_tokens")? as usize;
247 self.request_timeout_s = values.int("request_timeout_s")? as u64;
248 self.vision_max_pixels = values.int("vision_max_pixels")? as u64;
249 if self.max_tokens == 0 {
250 bail!("max_tokens must be positive");
251 }
252 Ok(())
253 }
254
255 async fn next(&mut self) -> Result<BenchmarkResult> {
256 let handle = self.handle()?.clone();
257 handle.check_cancelled()?;
258 if self.started.is_none() {
259 self.started = Some(Instant::now());
260 }
261
262 match self.phase {
263 Phase::Calibrate => self.calibrate_leg(&handle).await,
264
265 Phase::Geometry => self.geometry_leg(&handle).await,
266
267 Phase::Probes => {
268 let p = &PROBES[self.cursor];
269 let cell = self.run_probe(p).await;
270 let line = match &cell {
271 ProbeCell::Pass { id } => LogLine::info(format!("{id}: pass")),
272 ProbeCell::Fail { id, reply } => LogLine::warn(format!("{id}: {reply}")),
273 ProbeCell::Error { id, msg } => LogLine::warn(format!("{id}: {msg}")),
274 };
275 self.probes.push(cell);
276 self.cursor += 1;
277 if self.cursor >= PROBES.len() {
278 self.cursor = 0;
279 self.phase = Phase::Integrity;
280 }
281 Ok(self.frame("probes", vec![line]))
282 }
283
284 Phase::Integrity => self.integrity_leg().await,
285
286 Phase::Concurrency => self.concurrency_leg().await,
287
288 Phase::Control => {
289 let cell = self.run_probe(&CONTROL).await;
290 self.control_held = matches!(cell, ProbeCell::Pass { .. });
291 let line = if self.control_held {
292 LogLine::info("control: no image, no answer — capability results stand")
293 } else {
294 LogLine::warn(
295 "control: answered as though it saw an image — capability results are \
296 VACUOUS, the server may not be splicing vision embeddings at all",
297 )
298 };
299 self.phase = Phase::Score;
300 Ok(self.frame("control", vec![line]))
301 }
302
303 Phase::Score => {
304 self.phase = Phase::Done;
305 let integ_failed = self.integrity.iter().any(|c| c.measured() && !c.passed());
309 let concurrency_clean =
310 crate::benchmarks::video::concurrency::sweep_ok(&self.conc_results);
311 let v = with_runtime_checks(
312 verdict(&self.geom, &self.probes, self.control_held),
313 integ_failed,
314 concurrency_clean,
315 );
316 let asserted = asserted_cells(&self.geom);
317 let passed = self
318 .probes
319 .iter()
320 .filter(|c| matches!(c, ProbeCell::Pass { .. }))
321 .count();
322
323 let mut r = BenchmarkResult::running("score", self.elapsed());
324 r.status = if v == VisionVerdict::Pass {
325 RunStatus::Completed
326 } else {
327 RunStatus::Failed
328 };
329 r.summary = vec![
330 Stat::new("verdict", v.to_string(), ""),
331 Stat::new(
332 "geometry",
333 format!("{asserted}/{}", self.geom.len()),
334 "asserted",
335 ),
336 Stat::new(
337 "probes",
338 format!("{passed}/{}", self.probes.len()),
339 "passed",
340 ),
341 ];
342 r.metrics
343 .insert("geometry_asserted".into(), asserted as f64);
344 let matched = self
350 .geom
351 .iter()
352 .filter(|c| matches!(c, GeomCell::Match { .. }))
353 .count();
354 r.metrics.insert("geometry_matched".into(), matched as f64);
355 r.metrics
356 .insert("geometry_cells".into(), self.geom.len() as f64);
357 r.metrics.insert("probes_passed".into(), passed as f64);
358 r.metrics
359 .insert("probes_total".into(), self.probes.len() as f64);
360 r.metrics
361 .insert("control_held".into(), self.control_held as u8 as f64);
362 let baseline_prompt_tokens = self
365 .conc_results
366 .first()
367 .and_then(|baseline| baseline.prompt_tokens);
368 let conc_clean = self
369 .conc_results
370 .iter()
371 .filter(|r| {
372 baseline_prompt_tokens.is_some_and(|baseline| r.ok_against(baseline))
373 })
374 .count();
375 r.metrics
376 .insert("concurrency_levels_clean".into(), conc_clean as f64);
377 let integ_passed = self.integrity.iter().filter(|c| c.passed()).count();
378 let integ_measured = self.integrity.iter().filter(|c| c.measured()).count();
379 r.metrics
380 .insert("integrity_passed".into(), integ_passed as f64);
381 r.metrics
382 .insert("integrity_measured".into(), integ_measured as f64);
383 for lr in &self.conc_results {
384 r.metrics
385 .insert(format!("conc_{}_wall_ms", lr.conc), lr.wall_ms as f64);
386 }
387 r.verdict = Some(match v {
388 VisionVerdict::Pass => RunVerdict::pass(format!(
389 "{asserted} geometry cells matched, {passed}/{} probes, control held",
390 self.probes.len()
391 )),
392 VisionVerdict::Fail => {
393 let mism: Vec<String> = self
396 .geom
397 .iter()
398 .filter_map(|c| match c {
399 GeomCell::Mismatch { fixture, want, got } => {
400 Some(format!("{fixture} want {want} got {got}"))
401 }
402 GeomCell::Error { fixture, msg } => {
403 Some(format!("{fixture} ERROR {msg}"))
404 }
405 _ => None,
406 })
407 .collect();
408 let mut reason = format!(
409 "{matched}/{asserted} geometry matched, {passed}/{} probes",
410 self.probes.len()
411 );
412 if !mism.is_empty() {
413 reason.push_str(": ");
414 reason.push_str(&mism[..mism.len().min(5)].join("; "));
415 if mism.len() > 5 {
416 reason.push_str(&format!(" (+{} more)", mism.len() - 5));
417 }
418 }
419 let all_low = self.geom.iter().all(|c| match c {
424 GeomCell::Mismatch { want, got, .. } => got < want,
425 _ => true,
426 });
427 if self.vision_max_pixels == 0 && !mism.is_empty() && all_low {
428 reason.push_str(
429 " — every mismatch reads LOW: if the target serve passes \
430 --vision-max-pixels, set the vision_max_pixels param to the \
431 same value",
432 );
433 }
434 RunVerdict::fail(reason)
435 }
436 VisionVerdict::Vacuous => RunVerdict::fail(
437 "VACUOUS: the no-image control answered as though it saw one, so the \
438 capability probes are not evidence"
439 .to_string(),
440 ),
441 });
442 r.log = vec![match v {
443 VisionVerdict::Pass => LogLine::info(format!(
444 "PASS — {asserted} geometry cells asserted, {passed}/{} probes",
445 self.probes.len()
446 )),
447 VisionVerdict::Fail => LogLine::warn("FAIL — see the cells above"),
448 VisionVerdict::Vacuous => LogLine::warn(
449 "VACUOUS — the no-image control answered, so the capability probes are \
450 not evidence. Geometry results above are still valid.",
451 ),
452 }];
453 Ok(r)
454 }
455
456 Phase::Done => {
457 let mut r = BenchmarkResult::running("done", self.elapsed());
458 r.status = RunStatus::Completed;
459 Ok(r)
460 }
461 }
462 }
463}
464
465#[path = "driver_ladder.rs"]
466mod ladder;
467
468#[path = "driver_integrity.rs"]
469mod integrity;