๐Ÿ‘๏ธ vision_kit

August 16, 2026 ยท View on GitHub

English ยท ็ฎ€ไฝ“ไธญๆ–‡

vision_kit โ€” structured math-diagram reading with deterministic self-check

๐Ÿ‘๏ธ 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 demo: a plain VLM is confidently wrong vs vision_kit catches it

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_INCONSISTENT and retries with a targeted hint โ€” the mistake becomes an explicit signal.


Feature overview

The four-stage loop: read โ†’ verify โ†’ retry โ†’ diagnose

FeatureDescription
Structured extractionPull vectors / matrices / coordinates / angles / unknowns out of diagram images
Figure DOMTyped primitives โ€” points / segments / angles / circles / quadrilaterals (figures) with reference-integrity checks (endpoints/vertices/centers/quadrilateral vertices must be defined)
Dimension consistencyAuto-validate dimensions across related vectors/matrices; auto sample-retry to fill in missed numbers
Geometry self-checkTriangle 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 retryThe 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 / diagnosticsEvery 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 preprocessingUpscale 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-backRe-draw the extracted result as PNG (round-trip) for human/model verification of hallucinations
TikZ outputDOM โ†’ compilable LaTeX/TikZ (--tikz) so a diagram can be rebuilt for publishing/reuse
Stats โ†’ data tableBar/line/pie charts โ†’ categories + series values with length-alignment, percentage-sum โ‰ˆ 100 and non-negative checks (--mode stats)
Benchmark + snapshotsThree 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 integrationsOne 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:

ScenarioToolWhy
Non-math diagrams (photos, scenes, flowcharts, electrical/circuit schematics, general image Q&A)describeReturns 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)extractStructured extraction + dimension-consistency + geometry self-check โ€” its home turf
Want bothbothDescribe the whole, then extract structure

Worth knowing:

  • extract is 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 โ€” use describe instead.
  • describe doesn'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), describe accurately names the three-phase supply, PLC, switches and terminals; running extract on it can return invented fake vectors because there are no vector/matrix labels โ€” the classic case for "non-math diagram โ†’ use describe".


Why vision_kit?

Plugin / ProjectFocusWhere 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-deepeyeGeneric image Q&Aโœ… Adds deterministic self-consistency, not just prose
dsh-vision-toolkitOCR / 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)

Open-source benchmark scorecard

First published run (benchmark/REPORT.md, reproducible):

MetricValue
Numeric accuracy97.2% (35/36 components)
Recall10/10 labels
Hallucinations0
Dimension/reference consistency100%
Geometry self-consistency100%
Avg. model calls1.0 (no retries needed)
Avg. latency3.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. --baseline compares 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's glm-4v-flash? Set VISION_API_BASE to https://open.bigmodel.cn/api/paas/v4 and VISION_MODEL to glm-4v-flash, and remember to set VISION_MAX_TOKENS back to 1024 (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, then python -m vision_kit.* works from anywhere.
  • No install needed: add the repo directory to PYTHONPATH and import vision_kit directly (that's how the DSH plugin's visionKitDir works).

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.png ships 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, or cd to the repo root first. Image paths resolve against the current working directory.


Give a coding agent "eyes": no need to switch to a multimodal model โ€” any text-only model can see images.

Two tools

ToolPurpose
vision_describeLook at an image, return a description, or answer specific questions about it (pass prompt)
vision_extractStructured 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 cp above runs from the repo root. Want just the plugin, not the whole library? Copy that single file โ€” the plugin calls python -m vision_kit.cli, so the target machine still needs to import vision_kit (see "Do you need to pip install").

Credentials (priority)

  1. Env vars VISION_API_KEY / VISION_API_BASE / VISION_MODEL / โ€ฆ
  2. opencode config mcp["vision-kit"].env (global ~/.config/opencode/opencode.json or 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 usingConfigure hereNotes
opencode pluginVISION_API_KEY in opencode config mcp["vision-kit"].envReused automatically, simplest
CLI / MCP serverenv var VISION_API_KEY (or opencode.json env)export it, or set it in env
DSH pluginapiKey in vision.config.jsonsee 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.json holds 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_DIR env 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 set visionKitDir.

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'
  • name must be a file:// URL: a raw Windows path (D:\...) is rejected by the Node ESM loader as a d: scheme.
  • The plugin file is a CommonJS module (module.exports); tools are registered globally via ctx.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.png lives in the repo root. When using a global plugin / elsewhere, point to your own image's absolute path.


Environment variables

VariableRequiredDefault
VISION_API_KEYyes-
VISION_API_BASEnohttps://dashscope.aliyuncs.com/compatible-mode/v1
VISION_MODELnoqwen3-vl-flash
VISION_TIMEOUTno60
VISION_MAX_TOKENSno8192 (qwen3-vl-flash cap)
VISION_KIT_DIRnonone. 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 8192 is qwen3-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 to 1024~2048 rarely truncates and trims cost a little.
  • Returning a large matrix or an image with lots of text? Keep 8192 to 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-flash and its cap is only 1024 โ€” using 8192 errors with "parameter exceeds limit". Set VISION_MAX_TOKENS=1024.

By goal

GoalSuggested changeWhy
More accurate (ok with slower)VISION_TIMEOUT up, e.g. 120Preprocessing + retries + tiling can be slow; a tight timeout may false-fail
CheaperVISION_MAX_TOKENS to 1024~2048Structured output is usually plenty; avoids wasted tokens
Very small/dense imagesRaise 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-fillingRaise 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 of engine.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)