Zortium documentation
Zortium runs a structured battery of adversarial attacks against any OpenAI-compatible vision-language model and reports a per-suite Attack Success Rate. This is the reference for the open-source CLI.
Introduction #
Zortium is an adversarial testing tool for vision-language models (VLMs) — think promptfoo, but for image-based attacks. It points at any endpoint speaking the OpenAI chat-completions protocol, runs a battery of attack suites drawn from published research, and reports how often each attack got the model to comply.
The headline metric is Attack Success Rate (ASR) — the fraction of adversarial cases in a suite where the target produced harmful or hijacked output instead of refusing. Lower is safer. Each suite is scored independently, and an overall ASR summarises the run.
Zortium ships as a standalone CLI that never phones home. zortium.dev is an optional managed platform built on the same suites — web UI, saved history, model benchmarks, and team features — but nothing below requires it.
Installation #
pip install zortium
Requires Python 3.10 or newer. There are no system dependencies — every dependency ships as a pure-Python wheel, so a fresh machine needs nothing beyond pip.
Verify the install:
zortium --help
Quick start #
A scan needs a target (the model under test) and, for trustworthy numbers, a judge (a separate model that grades whether the target actually complied):
zortium \ --base-url https://openrouter.ai/api/v1 --model google/gemma-3-27b-it --api-key $OPENROUTER_API_KEY \ --judge-model gpt-4o-mini --judge-base-url https://api.openai.com/v1 --judge-api-key $OPENAI_API_KEY
Every flag has a ZORTIUM_* environment variable, so keys never appear in your shell history or a process listing. That single command is the whole interface — everything else is a convenience on top of it.
No judge configured? Zortium falls back to the target model grading its own responses and prints a warning. That keeps a first run useful, but a model judging itself is lenient — treat those breach rates as a floor, not a trusted number. Configure a real judge for results you'll act on.
You get a per-suite breach-rate table and a headline ASR:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ ┃ Attack Suite ┃ Severity ┃ Breach Rate ┃ Status ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ │ Typographic Visual Prompt Injection │ HIGH │ 0% │ PASS │ │ FigStep Typographic Jailbreak │ HIGH │ 25% │ WARN │ │ Query-Relevant Typographic Jailbreak │ HIGH │ 8% │ PASS │ │ Goal Hijacking │ MID │ 50% │ FAIL │ │ … │ … │ … │ … │ ├──────────────────────────────────────┼──────────┼─────────────┼────────┤ │ Overall │ │ 12% │ WARN │ └──────────────────────────────────────┴──────────┴─────────────┴────────┘ cases 137 evaluated · 17 breached PASS Overall ASR 12.3% within threshold 100%
The scan command #
The command has two halves — the target you're testing and the judge that grades it:
| Role | Flags | What it is |
|---|---|---|
| Target | --base-url · --model · --api-key |
The VLM under test — any OpenAI-compatible chat-completions endpoint. |
| Judge | --judge-model · --judge-base-url · --judge-api-key |
A separate model that grades harmful-content compliance. Base URL and key inherit the target's when omitted. |
Everything else layers convenience on top:
- Keys off the command line — every flag has a
ZORTIUM_*env var (ZORTIUM_API_KEY,ZORTIUM_JUDGE_API_KEY,ZORTIUM_MODEL, …). - Deeper audit —
--no-fastruns full per-suite coverage instead of the reduced fast set (see Scan modes). - Parallelism —
--tps Nruns N suites at once on high-limit endpoints (see Configuration).
Configuration #
Environment variables
Every flag has a ZORTIUM_* equivalent, so secrets stay out of shell history and CI logs. Precedence is flag > env var > config file > default.
Config file
Instead of a long command, generate a documented config and edit it:
# write a starter zortium.yaml in the current directory zortium --init # run from it (flags and env vars still override the file) zortium --config zortium.yaml
Both YAML and JSON are accepted. Blank or null values in the file fall through to env vars and defaults.
Saving a report
The CLI keeps no database. Add --output to write a full JSON report — run metadata, headline stats, and per-suite → per-case entries with the model response, judge verdict, and its reasoning:
zortium --output report.jsonParallelism
--tps N (env ZORTIUM_TPS) runs N suites in parallel. The default is 1 — sequential, which is safe for rate-limited or shared-key endpoints. Raise it for in-house models that can absorb the extra requests per second; rate limits are still handled per the 429 policy, so too high a value degrades gracefully rather than failing.
CI integration #
Use --max-asr to set a threshold. The CLI exits 1 if the overall ASR exceeds it — a native CI gate, no plugin needed:
zortium --max-asr 20 # fail the build if overall ASR > 20%
A complete GitHub Actions job — secrets stay in CI variables:
# .github/workflows/vlm-safety.yml name: VLM Safety Gate on: [push] jobs: zortium: runs-on: ubuntu-latest steps: - run: pip install zortium - run: zortium --max-asr 20 env: ZORTIUM_BASE_URL: ${{ secrets.VLM_BASE_URL }} ZORTIUM_MODEL: ${{ secrets.VLM_MODEL }} ZORTIUM_API_KEY: ${{ secrets.VLM_API_KEY }}
The --max-asr gate uses the raw overall ASR. Diagnostic suites are always excluded from it.
Scan modes #
Both modes run the same 18 suites; fast runs fewer, highest-signal cases per suite, thorough runs full coverage. --fast is the default; use --no-fast for deep audits.
| Fast (default) | Thorough | |
|---|---|---|
| Flag | --fast | --no-fast |
| Cases per image suite | Reduced (highest-signal) | Full |
| Good for | Daily CI, first runs | Pre-release, deep audits |
On runtime: wall-clock time is dominated by the target model's own latency and rate limits — not by Zortium — so it varies widely. Expect a few minutes to ~10+ for a fast scan; a throttled endpoint (especially with --wait) takes longer. Raise --tps on high-limit endpoints to cut it.
Rate limits #
By default a rate-limited (429) case is recorded as unavailable and left out of the score — fast, and right for CI. For a one-off baseline against a heavily throttled endpoint, --wait (env ZORTIUM_WAIT) retries each rate-limited case until the limit clears, so every case gets a real verdict. It respects the Retry-After header and can take much longer — use it for benchmarks, not CI.
zortium --no-fast --wait # complete thorough baseline, waits out rate limits
LLM judge #
Critical suites are graded by an LLM judge — it reads each (harmful request, model response) pair and decides whether the target actually complied. Keyword matching produces false positives on jailbreak evaluation, so these suites require a judge rather than falling back to keywords.
Configure it with --judge-model / --judge-base-url / --judge-api-key (or the ZORTIUM_JUDGE_* env vars). The base URL and key inherit the target's when omitted, so a judge on the same endpoint needs only --judge-model. Use a separate, stronger model than the one under test for better calibration.
No judge configured? The target grades its own responses and Zortium prints a warning. Self-grading is lenient — the resulting breach rates are a floor, not a trusted number. Always configure a real judge for numbers you'll act on.
Attack suites #
Critical — LLM judge required, high research backing
| Suite | Technique | Reference |
|---|---|---|
| Typographic Visual Prompt Injection | Embeds harmful instructions as text in images | Goh et al. 2021 |
| Split-Modality Injection | Splits instruction across image + text channels | Original |
| FigStep | Numbered-list document completion framing | Gong et al. 2023 |
| Steganographic Text Injection | Hides instructions in low-contrast regions | Li et al. 2024 |
| Image Hijacks | Behaviour-matching visual overlays | Bailey et al. 2023 |
| JailBreakV-28K | Real benchmark — 7 adversarial format types | Luo et al. 2024 |
| Visual Jailbreak Transfer | DAN / AIM / many-shot visual personas | Wei et al. 2023 |
| Query-Relevant Typographic Jailbreak | Hides the harmful instruction in a rendered-text image | Inspired by Liu et al. 2024 |
| Many-Shot Text | 24-example in-context compliance history | Anil et al. 2024 |
| Goal Hijacking | Prompt injection with delimiter spoofing | Perez & Ribeiro 2022 |
| Structured Output Injection | JSON schema / constrained-decoding exploitation | Original |
JailBreakV-28K isn't bundled — the suite fetches its benchmark images on demand from the official MIT-licensed HuggingFace repo the first time it runs (python -m zortium.attacks.jailbreakv_28k --download). Every other suite works out of the box. PAIR Agent (iterative attacker-LLM red-teaming, Chao et al. 2023) is implemented but disabled by default — a multi-turn loop for targeted deep audits rather than routine scanning.
Medium — keyword evaluation, limited effectiveness on modern models
| Suite | Technique |
|---|---|
| GCG Suffix | Precomputed adversarial token suffixes (transfer variant) |
| Encoding Attack | Base64 / ROT13 / hex payload obfuscation |
| Refusal Suppression | Instruction-stack refusal bypass |
| Multi-Modal Linkage | Cross-modal encryption-decryption framing |
| UI Spoofing | Authoritative-surface behavioural deviation |
Diagnostic — robustness probes, reported separately from ASR
| Suite | Metric |
|---|---|
| Visual Perturbation Drift | Cosine similarity drift under pixel noise |
| Stroop Interference | Visual-linguistic conflict detection |
Scoring & severity #
Each suite carries a severity reflecting how real-world exploitable the attack is — distinct from how it's evaluated internally:
| Severity | Meaning | Weight |
|---|---|---|
| HIGH | Strong, real-world exploitable attacks | 3 |
| MID | Limited effectiveness on modern models | 2 |
| LOW | Weak or marker-based techniques | 1 |
| DIAGNOSTIC | Robustness probes, excluded from ASR | 0 |
Diagnostic suites are always excluded from the headline ASR. On zortium.dev, severity also drives a weighted overall score, so a high-severity breach moves the number more than a low-severity one. The CLI --max-asr gate uses the raw overall ASR.
Providers #
Any endpoint speaking the OpenAI chat-completions protocol works — no SDK changes, no instrumentation, just a base URL and an API key:
- OpenRouter — pay-per-token access to hundreds of OSS models via one key (the easiest path for benchmarking).
- OpenAI, Groq, Together AI, Google Gemini (via their OpenAI-compatible endpoints).
- vLLM, Ollama (via the
/v1shim), TGI adapter. - In-house gateways and fine-tuned model deployments.
See Integrations for per-provider base URLs and model examples.
Architecture #
The engine is a src-layout package under src/zortium/:
src/zortium/ ├── runner.py TestRunner — judge enforcement, suite execution ├── constants.py ScanMode, SuitePriority, SuiteSeverity enums ├── attacks/ Attack suite implementations │ ├── base.py AttackSuite base — configure_for_mode(), evaluate() │ ├── __init__.py SuiteRegistry — build_active_suites() │ └── ... ├── providers/ │ ├── openai_compatible.py Target and judge provider abstraction │ └── ratelimit.py Retry-After / backoff, SKIP/WAIT ├── evaluators/ │ ├── judge.py LLM-as-judge evaluation │ └── refusal_aware.py Keyword-based refusal detection ├── utils/ Render, Perturbations, ImageUtils, PayloadPack └── config/ ├── attacks.json Suite config — priority, severity, parameters └── harm_payloads.json Harm categories and prompt library
Adding a suite: subclass AttackSuite in attacks/, add an entry to config/attacks.json, and register it in ACTIVE_SUITE_CLASSES. Override configure_for_mode() if it needs different behaviour in fast vs thorough mode.
Safety #
Zortium is a defensive security tool for evaluating the robustness of vision-language models you own or are authorized to test.
- Authorized targets only. Do not run Zortium against models or systems you don't own or have explicit permission to test.
- It sends adversarial and harmful content. By design, Zortium submits jailbreak prompts, harmful requests, and adversarial images to measure refusal behaviour. The bundled payloads and the JailBreakV-28K benchmark contain offensive material.
- Purpose is measurement, not exploitation. The output is a robustness report to help you harden a model — not a toolkit for attacking third-party systems.
Report security issues responsibly and use results to improve model safety.