AI Integration

Vision Evals

Find out which vision model you can trust to drive your UI, before it clicks anything.

Overview

@nut-tree/vision-evals is a benchmark harness for the vision models you plug into nut.js through @nut-tree/plugin-ai-sdk. It runs a suite of UI screenshots through the plugin's real matching pipeline, checks whether the model's answers are safe to click, and ranks candidate models against each other. It runs on your machine, against the backend you choose, and never uploads your screens anywhere.

bash
npx @nut-tree/vision-evals run --provider openai-compatible \
  --model mlx-community/Qwen3-VL-8B-Instruct-4bit \
  --base-url http://localhost:1234

Real Pipeline

No mocks, no simplified prompts. Every case calls the plugin's actual vision finder.

vision-evals run --provider openai

Hallucination First

Negative cases catch models that invent UI. Comparisons rank hallucination resistance above everything else.

neg pass 12/12

Head-to-Head

Mix local and hosted backends in one config and get a ranked comparison.

vision-evals compare --config targets.json

The Problem It Solves

With @nut-tree/plugin-ai-sdk, nut.js can locate UI elements from a plain-language description:

typescript
await screen.find(contentMatchingDescription("the blue 'Sign in' button"));

That single line hands a screenshot to a vision model and clicks whatever comes back. Which model should be on the other end? Public vision leaderboards do not answer that. They score captioning, document QA or general visual reasoning, and none of that tells you whether a model can put a box on the right button, in the right coordinate system, and stay quiet when the button is not there.

The only meaningful test is to run a model through the exact pipeline that ships: the plugin's system prompt, its structured-output schema, its confidence filtering, its region sanitisation and its coordinate handling. That is what vision-evals does. There are no mocks and no simplified prompts. Every case builds the same match request that nut.js core produces and calls the real vision finder, so the numbers you get predict production behaviour rather than paper scores.


What It Measures

Every case is one question asked of one screenshot. There are two kinds.

Positive cases describe something that is on screen and check whether the model finds it. The pass criterion is deliberately practical: the center of the returned region has to land inside the labelled element, because that center is exactly where nut.js will click. A generous box still passes if its center is on target. A tight box that is one element off fails.

Negative cases describe something that is not on screen, such as an absent dialog, a button that does not exist, or a chart that is not rendered, and check that the model returns nothing. A model that "finds" absent content will click arbitrary coordinates in production. This is the most dangerous failure in UI automation, and vision-evals treats it that way.

Metrics

For each case the harness records:

MetricCasesWhat it tells you
Center hitpositiveWhether a click on the returned region would land on the element. Pass or fail.
IoUpositiveHow well the returned box overlaps the true bounds, 0 to 1.
Center distancepositiveHow far off the box is, in pixels. Systematic offsets reveal coordinate-space problems.
HallucinationnegativeWhether the model invented a match for absent content. Pass or fail.
ConfidencebothThe model's self-reported confidence, including how confidently it hallucinated.
LatencybothWall-clock time per finder call in milliseconds, with mean and p95 in reports.

Failure Classes

Failures are classified so you know what to fix:

  • no-match: the element is there, the model did not return it. A recall problem, often a threshold that is too strict for that model.
  • miss: the model returned a box, but the click would land outside the element. A localisation problem, frequently a coordinate-space mismatch.
  • hallucination: the model returned matches for content that does not exist. A precision problem, and the one to weigh most heavily.
  • error: the backend failed. Not a verdict on the model.

Reports aggregate these into overall pass rate, recall, negative pass rate and precision, and keep every component visible. There is no opaque single score. When models are compared, they are ranked by hallucination resistance first, then by positive pass rate, then by latency.


What It Can Do

Benchmark One Backend

vision-evals run executes every case against a single model, prints a summary table, writes per-case results as JSONL and exits non-zero when cases fail. That makes it a drop-in gate for CI: qualify a model once, then re-run on every plugin upgrade or model update to catch regressions.

Compare Models Head to Head

vision-evals compare --config targets.json runs the same suite against any number of backends and prints a ranked comparison. A single config can mix a local vLLM or LM Studio server, an Ollama model, OpenAI and Anthropic, so you can put a self-hosted 8B model next to a hosted frontier model and see exactly what you gain or lose. API keys are referenced by environment variable name and never stored in the config.

A/B Coordinate Conventions

Vision model families are trained on different coordinate systems. Some answer in screenshot pixels, others in a normalised 0 to 1000 grid, and sending the wrong convention through the plugin turns a capable model into one that misses everything by a constant factor. A compare config can list the same model twice with different coordinateSpace settings, so the right convention for your model is a measured fact rather than a guess.

Measure Stability, Not Just a Single Lucky Run

Model answers are not deterministic, even at temperature zero. Setting runs to 5 or more repeats every case and exposes the flakiness an automation author would actually experience. A model that passes 13 of 15 positives on one run and 15 of 15 on the next is telling you something a single run cannot.

See Where the Model Looked

Pass --screenshots and every case is rendered as an annotated image: the ground-truth box in green, the model's box in red and the click point in magenta, collected in a browsable HTML gallery grouped by fixture. Because the overlays are drawn from the recorded results, any past run can be re-rendered with vision-evals screenshots <run-id> without calling the model again. Numbers tell you how wrong a match was. The gallery tells you where.

Bring Your Own Screens

The package ships with a ready-made fixture suite, so the first run needs nothing but a backend. The bundled scenarios cover:

FixtureScenarioCases
loginA login form6 positive, 4 negative
dashboardAn analytics dashboard with cards, charts and tables8 positive, 4 negative
settingsA settings dialog over a dimmed backdrop7 positive, 4 negative
precisionAn invite form of visually identical fields, checkboxes and buttons that differ only by label17 positive, 5 negative

Your own application will look different, and the suite is designed to be extended. Ground truth is a flat, self-contained JSON format listing element bounds in screenshot pixels. It can be:

  • generated from an HTML source, where the DOM layout gives pixel-perfect bounds,
  • imported from an accessibility tree with vision-evals import-a11y, which flattens nested layouts and applies scale and origin corrections once, or
  • handcrafted for desktop applications that expose no usable accessibility information at all.

The harness never transforms coordinates. Whatever the ground truth states is what gets asserted.

Script It

The CLI is the primary interface, but every building block is exported from the package: fixture discovery, finder construction, the eval runner, scoring, JSONL recording, aggregation and rendering. Custom pipelines, such as running a subset of cases nightly or feeding results into your own dashboard, can be assembled from the same parts.


Why It Is Useful

  • It answers the question that matters. Not "which model is smartest" but "which model can I let click on my application". Those are different questions, and only the second one has consequences for your automation.
  • It catches the failures that hurt. A model that hallucinates UI does not fail loudly. It clicks somewhere plausible and your workflow continues in the wrong state. Negative cases and the hallucination-first ranking make this failure mode visible before it reaches production.
  • It makes self-hosting a measured decision. Small local models are cheap, private and fast, and some of them are very good at UI localisation. Others are not, or are only good in the right coordinate convention. vision-evals lets you find the cheapest model that meets your bar instead of defaulting to the largest one.
  • It keeps your data on your machine. Screenshots, ground truth and results never leave your environment except as requests to the backend you configure. For a fully local backend, nothing leaves at all, which makes the tool suitable for private application screens.
  • It protects you across upgrades. Because the harness runs the plugin's actual pipeline, a score is only meaningful for the plugin version that produced it. Re-qualifying a model after a plugin upgrade is a matter of bumping one version and re-running.
  • It turns a model choice into a repeatable artifact. Results are JSONL files, reports render as text, Markdown or JSON, and the exit code is CI-friendly. The decision to adopt a model, and the evidence behind it, live in your repository next to the code that depends on it.

Versioning

The eval package is released in lockstep with @nut-tree/plugin-ai-sdk, and equal version numbers belong together. Installing @nut-tree/vision-evals@X pulls in @nut-tree/plugin-ai-sdk@X.

Reading a Report

text
model              run    cases  pass %  pos pass  recall %  neg pass  neg %   prec %  no-match  miss  halluc  errors  mean IoU  dist px  lat ms  p95 ms
-----------------  -----  -----  ------  --------  --------  --------  ------  ------  --------  ----  ------  ------  --------  -------  ------  ------
openai/gpt-5-mini  run-…  27     96.3%   14/15     93.3%     12/12     100.0%  100.0%  1         0     0       0       0.612     11.3     4210    9800

A few rules of thumb:

  • Negative pass rate below 100 % means the model hallucinates UI. Weigh this above every other column.
  • Recall below precision means the model refuses more than it errs. Loosening the confidence threshold usually helps.
  • Precision below recall means it returns boxes it should not. Tighten the threshold or prefer another model.
  • Many misses with a center distance near half or double the expected value points to a coordinate-space problem, not a comprehension problem. Try the other convention.
  • A drop in mean IoU between runs of the same model is a regression signal even when pass rates hold.

Getting Started

bash
# Evaluate a local OpenAI-compatible server (vLLM, LM Studio, llama.cpp, ...)
npx @nut-tree/vision-evals run \
  --provider openai-compatible \
  --model <your-model-id> \
  --base-url http://localhost:1234

# Evaluate a hosted provider
OPENAI_API_KEY=... npx @nut-tree/vision-evals run --provider openai

# Compare several backends, with annotated screenshots and a Markdown report
npx @nut-tree/vision-evals compare --config targets.json --screenshots --format markdown

Ready-to-edit compare configs ship in the package under example-configs/: a local quickstart, a coordinate-space A/B, a local multi-model shootout and a hosted OpenAI plus Anthropic example.

Commands

CommandPurpose
vision-evals runRun all cases against one backend. CI-friendly exit code.
vision-evals compare --config <file>Run against multiple backends and print a ranked comparison.
vision-evals summarize [run-id]Aggregate recorded results. --format markdown for reports, --failures for per-case detail.
vision-evals screenshots [run-id]Render annotated screenshots and an HTML gallery for a recorded run.
vision-evals import-a11y <tree.json>Convert an accessibility tree into ground truth.
vision-evals listShow discovered fixtures and their cases.

Every command supports --help, and run options fall back to EVAL_* environment variables for headless CI setups.

Supported Backends

  • openai-compatible: vLLM, LM Studio, llama.cpp and any other server speaking the OpenAI chat API
  • openai
  • anthropic
  • ollama

Where to Go Next

The package ships its full option reference in its README, guidance on writing discriminating positive and tempting negative cases in fixtures/README.md, and the compare config format in example-configs/README.md.

Was this page helpful?