๐๏ธ vision_kit
August 16, 2026 ยท View on GitHub
English ยท ็ฎไฝไธญๆ
๐๏ธ vision_kit
Make your AI agent a math tutor.
Reading math / geometry diagrams isn't about seeing โ it's about seeing correctly. vision_kit extracts vectors, matrices, coordinates and angles from figures, then verifies the data for dimensional consistency and geometric self-consistency โ filling gaps by retry and tiling large images automatically. If the numbers don't add up, you don't get a plausible-looking mistake; you get evidence.
Python ยท OpenAI-compatible vision ยท MCP ยท DeepSeek Harness ยท opencode
It's the vision plugin that actually verifies. Existing plugins just look (generic image Q&A). vision_kit adds a deterministic validation layer on top of the VLM output:
triangle angles โ 180ยฐ ยท vector addition mismatch ยท matrix dimension mismatch ยท negative/out-of-range value
Any failure triggers a sampling retry; when retries are exhausted, the โ / โ results are returned to the agent line by line, so it can spot "the VLM misread the number" instead of blindly accepting a wrong answer.

Adversarial sample: the diagram labels โ A=60ยฐ โ B=60ยฐ โ C=70ยฐ (angle sum 190ยฐ). A plain VLM trusts the labels and answers; vision_kit's validation layer sees 190ยฐ โ 180ยฐ, returns
โ GEOMETRY_INCONSISTENTand retries with a targeted hint โ the mistake becomes an explicit signal.
Feature overview

| Feature | Description |
|---|---|
| Structured extraction | Pull vectors / matrices / coordinates / angles / unknowns out of diagram images |
| Figure DOM | Typed primitives โ points / segments / angles / circles / quadrilaterals (figures) with reference-integrity checks (endpoints/vertices/centers/quadrilateral vertices must be defined) |
| Dimension consistency | Auto-validate dimensions across related vectors/matrices; auto sample-retry to fill in missed numbers |
| Geometry self-check | Triangle angle sum, vector addition, matrix multiplicability, segment length vs. endpoint distance, triangle inequality, Pythagoras, circle theorems (point-on-circle โ distance to center = radius; diameter subtends a right angle), similar triangles, parallelogram opposite sides equal, function-graph axis of symmetry, negative/range sanity โ pure logic, zero extra dependencies |
| Targeted gap-filling retry | The specific rule that failed (dimension / angle sum / vector add / side length โฆ) flows back into a targeted retry prompt, so the 2nd/3rd sample re-reads the image knowing where it went wrong |
| Confidence / diagnostics | Every result carries diagnostics: an error code (DIMENSION_MISMATCH / GEOMETRY_INCONSISTENT / NO_NUMERIC_CONTENT โฆ), overall + per-item confidence, and retry count โ so the agent can act on the code |
| Image preprocessing | Upscale small images for legibility; auto 2ร2 overlapping tiling + merge-and-recheck for very large images (cross-tile vector/geometry relations survive); describe auto-tiles huge images too |
| Render-back | Re-draw the extracted result as PNG (round-trip) for human/model verification of hallucinations |
| TikZ output | DOM โ compilable LaTeX/TikZ (--tikz) so a diagram can be rebuilt for publishing/reuse |
| Stats โ data table | Bar/line/pie charts โ categories + series values with length-alignment, percentage-sum โ 100 and non-negative checks (--mode stats) |
| Benchmark + snapshots | Three benchmark/ suites โ 12 synthetic diagrams (accuracy/recall/hallucination/consistency/geo/retry, --baseline quantifies the validation layer) + 6 adversarial-injection diagrams (contradictory annotations โ validation-layer protection rate) + 12 hard diagrams (perturbed images โ value of gap-filling retry); tests/snapshots/ numeric regression, auto-compared in CI |
| Multiple integrations | One engine, many surfaces: DSH plugin / opencode plugin / MCP / CLI |
๐ค Which tool: describe or extract?
The two tools solve different jobs โ pick the right one and each shines:
| Scenario | Tool | Why |
|---|---|---|
| Non-math diagrams (photos, scenes, flowcharts, electrical/circuit schematics, general image Q&A) | describe | Returns natural-language description / answers; generalizes well and never forces a structure that isn't there |
| Math diagrams with numeric annotations (vectors / matrices / point coords / angles / lengths / unknowns) | extract | Structured extraction + dimension-consistency + geometry self-check โ its home turf |
| Want both | both | Describe the whole, then extract structure |
Worth knowing:
extractis math-diagram-specific. It forces vector/matrix/coordinate output. On images with no such structure (layout diagrams, UIs, photos), the model may invent vectors that don't exist to satisfy the format โ usedescribeinstead.describedoesn't check structure, so it suits generic viewing, but on precise-number diagrams it won't give you trusted, dimension-checked structured data.- How to tell it's a math diagram: do you need to read exact sequences of numbers off the image (vectors, matrices, coords, angles)? Yes โ
extract. No โdescribe.
Example: on an electrical schematic (black background),
describeaccurately names the three-phase supply, PLC, switches and terminals; runningextracton it can return invented fake vectors because there are no vector/matrix labels โ the classic case for "non-math diagram โ usedescribe".
Why vision_kit?
| Plugin / Project | Focus | Where vision_kit differs |
|---|---|---|
dsh-vision (view_image) | Generic image Q&A | โ Only one producing structured math/geometry output |
dsh-tool-see-image (see_image) | Generic description | โ Has dimension checks, geo checks, gap-filling retry |
dsh-plugin-deepeye | Generic image Q&A | โ Adds deterministic self-consistency, not just prose |
dsh-vision-toolkit | OCR / UI reconstruction | โ Targets math annotations and validates value sanity |
In one line: the "read a diagram + verify geometry" combo is unique. vision_kit puts validation after the VLM output โ pure logic, unit-testable, benchmarkable, and independent of any specific vision model.
๐ Benchmark (12 synthetic diagrams ยท qwen3-vl-flash)

First published run (benchmark/REPORT.md, reproducible):
| Metric | Value |
|---|---|
| Numeric accuracy | 97.2% (35/36 components) |
| Recall | 10/10 labels |
| Hallucinations | 0 |
| Dimension/reference consistency | 100% |
| Geometry self-consistency | 100% |
| Avg. model calls | 1.0 (no retries needed) |
| Avg. latency | 3.1s/image |
The only imperfect row (
tri_sides_01) is a slight vertex-coordinate reading drift that still passes every geometry check (within tolerance) โ the layer never flags a correct reading as wrong.--baselinecompares single-shot greedy output vs. the validated+retried pipeline; on this simple synthetic set the deterministic layer is a safety net (never trust a hallucination) rather than a score booster โ retries pay off on harder, dense/noisy real-world diagrams.
First time? Confirm these
1. Get a vision-model key (Alibaba Cloud Bailian / DashScope)
The recommended model qwen3-vl-flash comes from Alibaba Cloud Bailian. Go to dashscope.console.aliyun.com โ sign in/register โ enable Bailian โ create an API Key (SK- prefix). Every integration (CLI / MCP / plugin) uses this same key.
2. Which integration to try first?
- Want to see results fast โ use the CLI (one command, instant JSON).
- Want your opencode agent to see images โ use the opencode plugin (recommended for developers).
- Want to call it from your own Python script โ use Quickstart (Python API).
Below is ordered CLI โ opencode plugin โ DSH plugin. Jump to the section you need.
3. Which directory?
The repo root is the package root (see package-dir in pyproject.toml). Run commands and place images in the cloned repo root so python -m vision_kit.* and test_figure.png resolve.
Quickstart (Python)
๐ก Recommended vision model:
qwen3-vl-flashโ a lightweight multimodal model from Alibaba Cloud Bailian (DashScope), stable on Chinese math/geometry figure annotations, good speed-to-cost. vision_kit calls via the OpenAI-compatible protocol, so you configure it through env vars โ no code changes needed. Prefer Zhipu'sglm-4v-flash? SetVISION_API_BASEtohttps://open.bigmodel.cn/api/paas/v4andVISION_MODELtoglm-4v-flash, and remember to setVISION_MAX_TOKENSback to1024(glm-4v-flash's cap is 1024 โ see "Tuning parameters" below). Examples below default to the DashScope (Bailian) compatible endpoint.
# Run from the repo root after cloning
pip install -e . # installs openai / pillow / mcp
export VISION_API_KEY=your_key # the SK- key from Alibaba Cloud Bailian
# The two below are optional defaults (official DashScope endpoint + qwen3-vl-flash); safe to omit:
export VISION_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1 # optional
export VISION_MODEL=qwen3-vl-flash # optional
from vision_kit import VisionClient
client = VisionClient(api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key="your_key", model="qwen3-vl-flash")
text = client.describe("problem_figure.png") # rendered text (for LLM injection)
data = client.describe_structured("problem_figure.png") # structured dict
Do you need to pip install?
vision_kit maps the repo root to the package root (pyproject.toml โ package-dir: vision_kit = ".").
Two ways to use it โ pick one:
- Recommended (easiest for CLI/MCP):
pip install -e .once, thenpython -m vision_kit.*works from anywhere. - No install needed: add the repo directory to
PYTHONPATHandimport vision_kitdirectly (that's how the DSH plugin'svisionKitDirworks).
Pick either one โ don't do both. If you neither install nor set PYTHONPATH, python -m vision_kit.cli will report "No module named".
Command Line (for plugins / scripts)
python -m vision_kit.cli problem_figure.png --mode describe # natural-language description / Q&A
python -m vision_kit.cli problem_figure.png --mode extract # structured extraction (JSON)
python -m vision_kit.cli problem_figure.png --mode both
python -m vision_kit.cli chart.png --mode stats # stats chart โ data table (categories/series)
python -m vision_kit.cli figure.png --mode extract --type geometry # figure-type prompt template
python -m vision_kit.cli figure.png --mode extract --tikz # also emit compilable TikZ
# after pip install: vision-kit-cli problem_figure.png --mode extract
Output is one line of JSON: {"ok": true, "mode": "extract", "result": {...}}; on failure {"ok": false, "error": "..."} with exit code 1.
Credentials come from env vars VISION_API_KEY / VISION_API_BASE / VISION_MODEL / VISION_TIMEOUT / VISION_MAX_TOKENS (same as the MCP server).
Quickest way to verify
# From the repo root (so the bundled test_figure.png is found)
python -m vision_kit.cli test_figure.png --mode extract
Note:
test_figure.pngships with this repo. When you use a global plugin / run from your own project, that file isn't in your workspace โ use your own image path, orcdto the repo root first. Image paths resolve against the current working directory.
opencode vision plugin (recommended)
Give a coding agent "eyes": no need to switch to a multimodal model โ any text-only model can see images.
Two tools
| Tool | Purpose |
|---|---|
vision_describe | Look at an image, return a description, or answer specific questions about it (pass prompt) |
vision_extract | Structured extraction of numeric annotations โ vectors / matrices / coordinates / angles, with dimension checks + gap-filling retry |
Install
The plugin is a JS file placed in the plugin directory; opencode loads it at startup (no config change):
# Prereq: clone this repo and use its .opencode/plugins/vision-kit.js
# Project-level (only the project it's added to): create the target plugin dir first if missing
mkdir -p <your-project>/.opencode/plugins
cp .opencode/plugins/vision-kit.js <your-project>/.opencode/plugins/
# Global (all projects):
mkdir -p ~/.config/opencode/plugins
cp .opencode/plugins/vision-kit.js ~/.config/opencode/plugins/
The
cpabove runs from the repo root. Want just the plugin, not the whole library? Copy that single file โ the plugin callspython -m vision_kit.cli, so the target machine still needs toimport vision_kit(see "Do you need to pip install").
Credentials (priority)
- Env vars
VISION_API_KEY/VISION_API_BASE/VISION_MODEL/ โฆ - opencode config
mcp["vision-kit"].env(global~/.config/opencode/opencode.jsonor project.opencode/opencode.json, as in opencode.example.json)
No need to repeat the key: as long as your opencode config has the vision-kit MCP, the plugin reuses the same credentials.
Where does the key go? One clean table:
| You're using | Configure here | Notes |
|---|---|---|
| opencode plugin | VISION_API_KEY in opencode config mcp["vision-kit"].env | Reused automatically, simplest |
| CLI / MCP server | env var VISION_API_KEY (or opencode.json env) | export it, or set it in env |
| DSH plugin | apiKey in vision.config.json | see DSH section below |
One key across the three spots โ you don't have to fill all of them; configure only the one for the integration you use.
How it works
The plugin implements no vision logic itself: it calls python -m vision_kit.cli and fully reuses this project's image preprocessing, structured extraction, dimension checks, gap-filling retry, and large-image tiling. The plugin only handles path resolution (relative paths resolve against the session directory), credential injection, and result formatting.
Try it
opencode run "use vision_describe on test_figure.png and tell me which vectors are in it"
opencode run "use vision_extract to extract structured data from test_figure.png"
test_figure.png is the bundled vector-problem example, in the repo root.
When running with a global plugin / from another workspace, use an absolute path to your own image.
opencode MCP server
python -m vision_kit.mcp_server # after pip install: vision-kit-mcp
Wire it into opencode via opencode.example.json (DashScope qwen3-vl-flash example).
DeepSeek Harness vision plugin
The DSH fixed Cordis plugin (dsh-plugin/vision-kit.dsh.js) is the same source as the opencode one.
It gives any text-only DSH model "eyes": registers vision_describe / vision_extract, and reuses all of vision_kit's vision logic through python -m vision_kit.cli.
vision_extract additionally returns the geometry self-check results (geo_checks) to the model โ triangle angle sum, vector-add relations, matrix dimensions, negatives, etc.
Any failure triggers a sampling retry; when retries are exhausted, each โ failed line is shown, so the text agent can notice "the reading contradicts itself" instead of trusting it blindly.
First-time setup (required)
The DSH plugin reads vision credentials from vision.config.json at the repo root. For security, this file isn't distributed โ you create your own:
# From the repo root
cp vision.config.example.json vision.config.json
# then edit vision.config.json:
# apiKey โ your SK- key from Alibaba Cloud Bailian (required)
# python โ (recommended) absolute path to your python.exe
# visionKitDirโ absolute path to this repo root (used to locate cli.py and vision.config.json)
- The template vision.config.example.json contains no secrets and is safe to use as a reference.
- โ ๏ธ
vision.config.jsonholds your API key โ treat it as private, don't share or upload it. - The plugin locates the project directory automatically: it checks the
VISION_KIT_DIRenv var first, then derives it from the plugin file's own location (this file lives at<repo-root>/dsh-plugin/vision-kit.dsh.js, so the repo root is its parent directory), and finally probes the current working directory; if it still can't find it, it errors and guides you to setvisionKitDir.
Install & enable (fixed plugin, survives restart)
The plugin is now a DSH fixed plugin: written as an insert line into $DSH_HOME/profiles/web/cordis.patch.yml,
it auto-mounts when the DSH Web deployment starts and survives restarts โ no need to cordis_define it again each time:
# $DSH_HOME/profiles/web/cordis.patch.yml
- insert:
- id: vision_kit
name: 'file:///D:/libs/vision_kit/dsh-plugin/vision-kit.dsh.js'
namemust be afile://URL: a raw Windows path (D:\...) is rejected by the Node ESM loader as ad:scheme.- The plugin file is a CommonJS module (
module.exports); tools are registered globally viactx.tools.register(...), so all sessions can use them right after mounting (new sessions immediately; already-open ones see them after the next start). - The equivalent manual overlay lives in dsh-plugin/cordis.yml (for
dsh web --patch). - After editing plugin code, restart
dsh web(editing the patch file itself triggers hot reload).
Paths
image_path accepts relative paths (resolved against the caller's session workspace) or absolute paths.
Which model calibrated the sample (important)
The test_figure.png results shown throughout this README were calibrated with the default model qwen3-vl-flash (DashScope).
Different vision models may extract slightly different values from the same image โ expected when you switch to glm or others.
Try it
use vision_describe on test_figure.png and tell me which vectors are in it
use vision_extract to extract structured data from test_figure.png
test_figure.pnglives in the repo root. When using a global plugin / elsewhere, point to your own image's absolute path.
Environment variables
| Variable | Required | Default |
|---|---|---|
VISION_API_KEY | yes | - |
VISION_API_BASE | no | https://dashscope.aliyuncs.com/compatible-mode/v1 |
VISION_MODEL | no | qwen3-vl-flash |
VISION_TIMEOUT | no | 60 |
VISION_MAX_TOKENS | no | 8192 (qwen3-vl-flash cap) |
VISION_KIT_DIR | no | none. DSH plugin project dir for PYTHONPATH; the plugin reads it first to locate the project, else auto-probes / guides via template |
Tuning parameters
The env vars and engine constants ship with sensible defaults โ most setups need no changes. Fine-tune only if you have specific needs:
About max_tokens
- The default
8192isqwen3-vl-flash's cap. Structured vision output is usually a short JSON (vectors/matrices/notes), often only a few hundred to a couple thousand tokens โ so lowering to1024~2048rarely truncates and trims cost a little. - Returning a large matrix or an image with lots of text? Keep
8192to avoid truncation; go above only if needed. - โ ๏ธ Always adjust this when switching models: defaults track qwen3-vl-flash (cap 8192).
Switch to Zhipu's
glm-4v-flashand its cap is only 1024 โ using 8192 errors with "parameter exceeds limit". SetVISION_MAX_TOKENS=1024.
By goal
| Goal | Suggested change | Why |
|---|---|---|
| More accurate (ok with slower) | VISION_TIMEOUT up, e.g. 120 | Preprocessing + retries + tiling can be slow; a tight timeout may false-fail |
| Cheaper | VISION_MAX_TOKENS to 1024~2048 | Structured output is usually plenty; avoids wasted tokens |
| Very small/dense images | Raise MIN_DIM in engine.py (default 1000) | More aggressive upscale โ crisper digits |
| Very large images (tiled) | Lower MAX_TILE_SIDE (default 1600) | Smaller tiles lighten per-model load but increase count/overhead |
| More thorough gap-filling | Raise MAX_ATTEMPTS (default 3) | More retries when validation fails; stabler yet slower |
Env vars (
VISION_*) are set via CLI / MCP / opencode plugin env โ no code change needed. Engine constants (MIN_DIM/MAX_TILE_SIDE/MAX_ATTEMPTS, etc.) are at the top ofengine.py; edit then re-run.
Development
pip install -e ".[dev]"
pytest # unit tests (tests/)
ruff check . # linting
# Benchmark: regenerate the diagrams (deterministic, no API needed)
python benchmark/generate_images.py # main + adversarial + hard suites
# Run evaluation (needs VISION_API_KEY): per-image metrics + REPORT leaderboard
python benchmark/eval.py --limit 3 # quick smoke run
python benchmark/eval.py # full main suite โ REPORT.md
python benchmark/eval.py --subset adv # adversarial suite โ REPORT_ADV.md (validation-layer protection rate)
python benchmark/eval.py --subset hard # hard suite โ REPORT_HARD.md (gap-filling retry value under perturbation)
python benchmark/eval.py --baseline # also run the single-shot greedy baseline to quantify the validation-layer gain
# Regression snapshots: record/compare real-model output baselines (needs VISION_API_KEY)
python benchmark/eval.py --update-snapshots
pytest tests/test_snapshots.py # numeric-level regression (auto-skipped offline)
Project structure
vision_kit/
โโโ __init__.py # package entry, exports VisionClient (__version__ = 0.3.0)
โโโ client.py # VisionClient: upscale preprocess / structured / tiled merge-recheck / targeted retry / render-back / content-addressed cache
โโโ engine.py # recognition engine: prompts, parameterized calls, targeted retry (failed rule โ hint), tiled merge-recheck for huge images
โโโ figure.py # FigureData: figure DOM (incl. on-circle points / quadrilaterals) + vector/matrix parse + dimension/reference checks + geo checks + tiled merge + render-back + TikZ
โโโ geometry.py # geometric self-check: triangle sum / vector add / matrix dims / segment length / triangle inequality / Pythagoras / circle theorems / similar triangles / parallelogram / function symmetry / negatives (pure logic)
โโโ stats.py # stats chart โ data table (4.3): categories/series + alignment / percent-sum / non-negative checks + retry
โโโ prompts.py # scenario prompt templates (4.1): geometry / function / vector / statistics
โโโ cli.py # command-line JSON entry (--mode describe|extract|both|render|stats, --type/--tikz/--render)
โโโ mcp_server.py # opencode MCP server (stdio; describe_image / structured / stats)
โโโ benchmark/ # 2.1 benchmark: synthetic + adversarial + hard suites + ground_truth* + eval.py (--subset/--baseline; produces REPORT*.md)
โโโ tests/snapshots/ # 2.4 regression snapshot baselines (real model output, numeric comparison)
โโโ opencode.example.json
โโโ vision.config.example.json # DSH plugin config template (no key, ready to use)
โโโ vision.config.json # local vision credentials (copied from template; not distributed)
โโโ .gitignore # excludes vision.config.json and other local files
โโโ dsh-plugin/
โ โโโ vision-kit.dsh.js # DeepSeek Harness fixed vision plugin (CommonJS module)
โ โโโ cordis.yml # equivalent overlay (for `dsh web --patch`; fixed install lives in $DSH_HOME/profiles/web/cordis.patch.yml)
โโโ .opencode/
โโโ plugins/vision-kit.js # opencode vision plugin (see above)