```
## What We're Building
The reviewer is a small Python project with a few clear parts:
| Part | What it does |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pydantic models | Define `Evidence`, `Finding`, and `Chain`, and give us a hard validation boundary between the LLM and the rest of the program |
| Venice client | Wraps the OpenAI Python SDK pointed at Venice's OpenAI-compatible endpoint |
| AST repo map | Walks the target tree with Python's `ast` module and builds a deterministic map of every module's public symbols and import edges |
| Scanner agent | Reads one Python file at a time plus a per-file neighbourhood slice of the repo map, and emits atomic vulnerability findings with file:line evidence |
| Chainer agent | Reads the union of findings plus a condensed full repo map, and emits exploit chains that combine two or more findings |
| Reference validator | Drops any chain that references a finding ID the Scanner did not produce, or names a file none of its referenced findings actually came from |
| Markdown report | Renders findings and chains into a human-readable report |
| CLI | Wires everything together with Typer |
The flow looks like this:
1. Walk the target directory for `.py` files.
2. Build a deterministic repo map (imports, public symbols, signatures).
3. For each file, send the Scanner its source plus a per-file neighbourhood slice of the map and collect atomic findings.
4. Send the union of findings plus the condensed repo map to the Chainer and collect exploit chains.
5. Drop any chain that references a finding ID the Scanner did not produce, or that names a file none of its referenced findings actually came from.
6. Write a Markdown report.
Two design decisions are worth flagging before we start writing code.
The first is **why two agents instead of one**. A single-agent scanner that tries to do everything in one prompt has to balance being thorough about per-file bugs against being clever about combinatorial reasoning. Splitting the work means the Scanner can be relentless and noisy, and the Chainer can be selective and quiet. Adding one extra LLM call dedicated to combining findings unlocks an entire class of bug for very little extra code.
The second is **why a repo map**. Real codebases live across many files. A bug that consists of "the validator runs but doesn't apply per-iteration in the fetcher, and the fetcher's response ends up in the renderer" is invisible to a per-file scanner. Before any LLM call, we walk the target tree with Python's `ast` and build a structural map. The Scanner sees a per-file *neighbourhood* (who imports from this file, what this file imports, signatures of those external symbols). The Chainer sees a *condensed* full map (every module, every public symbol, every import edge, no source). That's the smallest amount of context engineering we have found that lets the Chainer construct chains whose data flow crosses module boundaries, without paying the token cost of stuffing the whole codebase into every prompt.
## Pre-requisites
* Python 3.12+
* A Venice API key from [venice.ai](https://venice.ai)
* Basic familiarity with Pydantic, Python's `ast` module, and the OpenAI Python SDK
The reference repo uses [`uv`](https://docs.astral.sh/uv/) for dependency management, but a regular virtual environment works just as well.
## Setting Up the Project
Create a new project and install the dependencies:
```bash theme={"system"}
mkdir venice-security-reviewer
cd venice-security-reviewer
uv init
uv add "openai>=1.54" "pydantic>=2.9" "typer>=0.12" "jinja2>=3.1" "python-dotenv>=1.0" "rich>=13.0"
```
If you prefer `pip`, create a virtual environment instead:
```bash theme={"system"}
python -m venv .venv
source .venv/bin/activate
pip install "openai>=1.54" "pydantic>=2.9" "typer>=0.12" "jinja2>=3.1" "python-dotenv>=1.0" "rich>=13.0"
```
Create a `.env` file for local development:
```bash theme={"system"}
VENICE_API_KEY=your-venice-api-key-here
# Optional overrides
# VENICE_BASE_URL=https://api.venice.ai/api/v1
# VENICE_MODEL=zai-org-glm-5
```
We'll lay the source out under `src/venice_security_reviewer/` to keep it importable as a package, with prompts under `prompts/` at the repo root so they can be reviewed and diffed like any other source artefact:
```
src/venice_security_reviewer/
__init__.py
models.py # Pydantic models
client.py # Venice client factory
repo_map.py # AST-built repo map
scanner.py # Scanner agent
chainer.py # Chainer agent
report.py # Jinja2 Markdown rendering
cli.py # Typer CLI
templates/
report.md.j2
prompts/
scanner.md
chainer.md
tests/
test_models.py
test_cross_file_chain.py
```
## Setting Up the Venice Client
Venice is OpenAI-compatible, so we can use the official OpenAI Python SDK and just point its `base_url` at Venice. Centralising the client construction in one file means the rest of the code never has to know which provider it's talking to: swapping backends would only touch this one module.
Create `src/venice_security_reviewer/client.py`:
```python theme={"system"}
from __future__ import annotations
import os
from dataclasses import dataclass
from dotenv import load_dotenv
from openai import OpenAI
DEFAULT_BASE_URL = "https://api.venice.ai/api/v1"
DEFAULT_MODEL = "zai-org-glm-5"
class VeniceConfigError(RuntimeError):
"""Raised when Venice client config is missing or invalid."""
@dataclass(frozen=True, slots=True)
class VeniceConfig:
api_key: str
base_url: str
model: str
@classmethod
def from_env(cls) -> "VeniceConfig":
load_dotenv()
api_key = os.getenv("VENICE_API_KEY")
if not api_key:
raise VeniceConfigError(
"VENICE_API_KEY is not set. Add it to your .env file, "
"or export VENICE_API_KEY in your shell."
)
return cls(
api_key=api_key,
base_url=os.getenv("VENICE_BASE_URL", DEFAULT_BASE_URL),
model=os.getenv("VENICE_MODEL", DEFAULT_MODEL),
)
def build_client(config: VeniceConfig | None = None) -> tuple[OpenAI, str]:
cfg = config or VeniceConfig.from_env()
client = OpenAI(api_key=cfg.api_key, base_url=cfg.base_url)
return client, cfg.model
```
A few things worth noting:
* We default to `zai-org-glm-5` because it's a strong general-purpose Venice model, but you can override it with the `VENICE_MODEL` environment variable. For larger or more nuanced codebases, swapping in a stronger model can make the Chainer notably better at narrative quality.
* `build_client` returns the client *and* the model id, so callers don't have to read environment variables themselves and tests can inject a fake config without monkeypatching.
## Defining the Data Models
The whole point of using Pydantic here, rather than passing raw dicts around, is that we get a hard validation boundary between the LLM and the rest of the program. If the model returns malformed JSON or invents a finding ID that doesn't exist, parsing fails loudly and we never propagate the hallucination into the report.
Create `src/venice_security_reviewer/models.py`:
```python theme={"system"}
from __future__ import annotations
from pathlib import Path
from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
Severity = Literal["low", "medium", "high", "critical"]
ChainSeverity = Literal["high", "critical"]
class Evidence(BaseModel):
"""A concrete code span that justifies a finding."""
model_config = ConfigDict(frozen=True)
file: Path
start_line: int = Field(ge=1)
end_line: int = Field(ge=1)
snippet: str
@model_validator(mode="after")
def _check_line_range(self) -> Self:
if self.end_line < self.start_line:
raise ValueError(
f"end_line ({self.end_line}) must be >= start_line ({self.start_line})"
)
return self
class Finding(BaseModel):
"""An atomic vulnerability surfaced by the Scanner agent."""
model_config = ConfigDict(frozen=True)
id: str = Field(pattern=r"^F-\d{3,}$")
title: str = Field(min_length=1)
severity: Severity
description: str = Field(min_length=1)
cwe: str | None = None
evidence: Evidence
class Chain(BaseModel):
"""An exploit chain combining two or more atomic findings."""
model_config = ConfigDict(frozen=True)
id: str = Field(pattern=r"^C-\d{3,}$")
findings: list[str] = Field(min_length=2)
narrative: str = Field(min_length=1)
severity: ChainSeverity
files_involved: list[Path] = Field(min_length=1)
```
The constraints are doing real work here:
* `Finding.id` and `Chain.id` are constrained to a regex like `F-001`, `C-001`. If the model gets creative with the format, validation fails.
* `Chain.findings` requires at least two entries: a "chain" of one finding is just a finding.
* `Chain.severity` is restricted to `high` or `critical`. A combination of findings that doesn't raise the impact above the highest individual severity isn't a chain worth reporting.
* `Evidence` enforces that `end_line >= start_line` so the model can't return nonsensical line ranges.
That's the *shape* validation. We also need *cross-reference* validation: a chain that references a finding ID the Scanner never produced is meaningless. Add this function to `models.py`:
```python theme={"system"}
def validate_chain_references(
chains: list[Chain], findings: list[Finding]
) -> tuple[list[Chain], list[Chain]]:
findings_by_id = {f.id: f for f in findings}
valid: list[Chain] = []
dropped: list[Chain] = []
for chain in chains:
if not all(ref in findings_by_id for ref in chain.findings):
dropped.append(chain)
continue
chain_evidence_files = {
findings_by_id[ref].evidence.file.as_posix() for ref in chain.findings
}
if not all(p.as_posix() in chain_evidence_files for p in chain.files_involved):
dropped.append(chain)
continue
valid.append(chain)
return valid, dropped
```
This is the deterministic guardrail that keeps the Chainer honest. It can only reference findings the Scanner actually produced, and it can only claim files involved in the chain that one of those findings actually came from. Returning the dropped chains rather than silently filtering them lets the CLI surface a warning when the model tries to invent something.
## Building the AST Repo Map
The repo map is the structural skeleton of a Python codebase: every module's public surface, every import edge, and a reverse index from "module M" to "modules that import from M". It's built once per scan run with Python's `ast`, never via execution, so it's safe to run on adversarial code: the parser doesn't import or invoke anything from the scanned tree.
We'll consume the map in two shapes. The Scanner gets a per-file *neighbourhood* slice so its prompts stay bounded in size. The Chainer gets a *condensed* full map so it can construct chains across files.
Create `src/venice_security_reviewer/repo_map.py` and start with the Pydantic models that describe the map:
```python theme={"system"}
from __future__ import annotations
import ast
import logging
from collections.abc import Iterable
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
logger = logging.getLogger(__name__)
SymbolKind = Literal["function", "class", "constant"]
_SIGNATURE_CHAR_CAP = 200
SKIP_DIR_NAMES: frozenset[str] = frozenset({
".git", ".venv", "venv", "env", "__pycache__", "node_modules",
"dist", "build", ".mypy_cache", ".pytest_cache", ".ruff_cache",
"site-packages",
})
class SymbolDef(BaseModel):
model_config = ConfigDict(frozen=True)
name: str
kind: SymbolKind
line: int = Field(ge=1)
signature: str | None = None
class ImportEdge(BaseModel):
model_config = ConfigDict(frozen=True)
from_module: str
imported_names: list[str]
line: int = Field(ge=1)
class ModuleEntry(BaseModel):
model_config = ConfigDict(frozen=True)
path: Path
module_name: str
defines: list[SymbolDef]
imports: list[ImportEdge]
exports: list[str]
```
Now the helper that walks the tree and skips directories we shouldn't index:
```python theme={"system"}
def _iter_python_files(root: Path) -> Iterable[Path]:
for path in sorted(root.rglob("*.py")):
if any(part in SKIP_DIR_NAMES for part in path.parts):
continue
if path.is_file():
yield path
def _path_to_module_name(path: Path, root: Path) -> str:
rel = path.relative_to(root)
parts = list(rel.with_suffix("").parts)
if parts and parts[-1] == "__init__":
parts = parts[:-1]
return ".".join(parts)
```
For each file we want three things out of the AST: the top-level symbols it defines, the import edges, and an explicit `__all__` list if one is present. Function signatures and class headers get rendered as compact strings the LLM can read directly:
```python theme={"system"}
def _render_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
try:
prefix = "async def " if isinstance(node, ast.AsyncFunctionDef) else "def "
args = ast.unparse(node.args)
returns = f" -> {ast.unparse(node.returns)}" if node.returns is not None else ""
sig = f"{prefix}{node.name}({args}){returns}"
if len(sig) > _SIGNATURE_CHAR_CAP:
return f"{prefix}{node.name}(...)"
return sig
except Exception:
return f"def {node.name}(...)"
def _render_class_header(node: ast.ClassDef) -> str:
try:
bases = [ast.unparse(b) for b in node.bases]
sig = f"class {node.name}({', '.join(bases)})" if bases else f"class {node.name}"
if len(sig) > _SIGNATURE_CHAR_CAP:
return f"class {node.name}(...)"
return sig
except Exception:
return f"class {node.name}"
```
The `_SIGNATURE_CHAR_CAP` of 200 preserves typical real signatures (including type hints) while preventing pathological cases like a 200-line typed union from blowing up the prompt.
Next, the extractor that pulls the structural data out of a parsed module. We handle `ast.FunctionDef`, `ast.ClassDef`, top-level `ast.Assign` and `ast.AnnAssign` for constants, and both `ast.Import` and `ast.ImportFrom` for the import edges. Relative imports get resolved into their absolute dotted form so the Chainer can match them against module names later:
```python theme={"system"}
def _resolve_relative_package(
*, importer_module: str, importer_is_init: bool, level: int
) -> str | None:
if level <= 0:
return None
importer_parts = importer_module.split(".") if importer_module else []
base_parts = list(importer_parts) if importer_is_init else importer_parts[:-1]
steps_up = level - 1
if steps_up > len(base_parts):
return None
package_parts = (
base_parts[: len(base_parts) - steps_up] if steps_up else list(base_parts)
)
return ".".join(package_parts)
```
The full extraction logic walks `tree.body` and emits `SymbolDef` and `ImportEdge` entries for each top-level node. The reference repo's `_extract` function in [`repo_map.py`](https://github.com/joshua-mo-143/venice-security-agent-demo/blob/main/src/venice_security_reviewer/repo_map.py) covers the full implementation. The shape that comes out is a list of `ModuleEntry` objects, one per file.
The interesting part is what we do with those entries. Wrap them in a `RepoMap` with two consumer-facing methods:
```python theme={"system"}
class RepoMap(BaseModel):
model_config = ConfigDict(frozen=True)
root: Path
modules: list[ModuleEntry]
def by_module_name(self, module_name: str) -> ModuleEntry | None:
for m in self.modules:
if m.module_name == module_name:
return m
return None
def importers_of(self, module_name: str) -> list["ImportingRef"]:
refs: list["ImportingRef"] = []
for m in self.modules:
for edge in m.imports:
if edge.from_module == module_name:
refs.append(
ImportingRef(
importer_path=m.path,
importer_module=m.module_name,
imported_names=list(edge.imported_names),
line=edge.line,
)
)
return refs
def neighborhood(self, path: Path) -> "ModuleNeighborhood | None":
m = next((mod for mod in self.modules if mod.path == path), None)
if m is None:
return None
return ModuleNeighborhood(
this_module=m,
imported_by=self.importers_of(m.module_name),
imports_from_repo=self.resolve_imports_in_repo(m.module_name),
)
def condensed_dict(self) -> dict[str, object]:
return {
"modules": [
{
"path": str(m.path),
"module": m.module_name,
"exports": list(m.exports),
"imports": [
{"from": e.from_module, "names": list(e.imported_names)}
for e in m.imports
],
}
for m in self.modules
]
}
```
`neighborhood(path)` is what the Scanner calls for each file. It returns a `ModuleNeighborhood` object containing the module itself, every other module that imports from it, and every in-repo symbol it imports from elsewhere (with their resolved signatures). That gives the Scanner enough context to flag findings that are only obvious in cross-file context, without dragging the whole codebase into the prompt.
`condensed_dict()` is what the Chainer gets. Snippets and signatures are dropped; only paths, module names, public exports, and import edges remain. That's the smallest representation that still lets the Chainer reason about cross-module data flow.
Finally, the entry point that builds the whole thing:
```python theme={"system"}
def build_repo_map(root: Path) -> RepoMap:
root = root.resolve()
modules: list[ModuleEntry] = []
for path in _iter_python_files(root):
rel = path.relative_to(root)
module_name = _path_to_module_name(path, root)
is_init = path.stem == "__init__"
try:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source)
except (OSError, SyntaxError, UnicodeDecodeError) as exc:
logger.warning("repo_map: skipping %s: %s", rel, exc)
continue
defines, imports, explicit_all = _extract(
tree, importer_module=module_name, importer_is_init=is_init
)
exports = explicit_all or [s.name for s in defines if not s.name.startswith("_")]
modules.append(
ModuleEntry(
path=rel,
module_name=module_name,
defines=defines,
imports=imports,
exports=exports,
)
)
return RepoMap(root=root, modules=modules)
```
Files we can't read or that fail to parse get logged and skipped. We return a partial map rather than failing the whole run; the worst case is that a Scanner call sees no neighbourhood for one file, which is still a working scan.
## Writing the Scanner Agent
The Scanner walks a target path, picks up Python source files, and asks Venice to identify atomic vulnerabilities one file at a time. Per-file scanning keeps the prompt small and makes failures isolated: one bad file doesn't kill the whole run.
We'll keep the prompt itself in a separate file so it can be reviewed and diffed like any other source artefact. Create `prompts/scanner.md`:
````markdown theme={"system"}
You are a static security analyst reviewing a single Python source file for
vulnerabilities. You will be given the file path, its full contents, and a
*neighborhood* slice of the surrounding repo: which other modules import
from this file (and what symbols they pull), and which in-repo symbols this
file imports from elsewhere. You must respond with a JSON object that lists
every distinct vulnerability you can identify, with concrete file:line
evidence for each.
# Rules
1. Output a single JSON object. No prose before or after. No markdown fences.
2. The object must match this schema exactly:
```json
{
"findings": [
{
"id": "F-001",
"title": "Short imperative title, e.g. 'Hardcoded session signing key'",
"severity": "low | medium | high | critical",
"description": "One to three sentences explaining the vulnerability and why it matters.",
"cwe": "CWE-798 or null if not applicable",
"evidence": {
"file": "{filename}",
"start_line": 12,
"end_line": 14,
"snippet": "the exact lines from the source, copied verbatim including whitespace"
}
}
]
}
```
3. Finding IDs must be sequential within this file: F-001, F-002, F-003, etc.
4. The `file` field in evidence must equal the filename you were given, exactly.
5. `start_line` and `end_line` must be 1-indexed line numbers from the source you were given.
6. The `snippet` must be the exact text of those lines, copied verbatim. Do not paraphrase. Do not truncate.
7. Do not invent vulnerabilities. If you are unsure, omit it. False positives waste the operator's time and erode trust in the tool.
8. Every finding's evidence must point at lines in THIS file. Do not produce findings whose evidence lives in a different file. The Chainer is the agent that reasons across files.
9. If the file contains no vulnerabilities, return `{"findings": []}`.
````
The full prompt in the [reference repo](https://github.com/joshua-mo-143/venice-security-agent-demo/blob/main/prompts/scanner.md) also contains a "What to look for" section listing common vulnerability classes (hardcoded secrets, SQL injection, command injection, SSRF, insecure deserialization, etc.) and a "How to use the neighborhood" section explaining how the model should consume the cross-file context.
A few prompt design notes:
* We tell the model to emit JSON only, with no prose or fences. The OpenAI SDK supports a `response_format={"type": "json_object"}` parameter that enforces this on the API side, but reinforcing it in the prompt cuts down on edge cases.
* We explicitly forbid the Scanner from producing cross-file chains. Chains are the Chainer's job, and asking the Scanner to do both blurs the responsibility.
* We require the snippet to be copied verbatim. This means the report can quote the exact bytes the model claims to have seen, and a reviewer can spot-check a finding by comparing the snippet to the source.
Now the agent code. Create `src/venice_security_reviewer/scanner.py` and start with the file walker and prompt loader:
```python theme={"system"}
from __future__ import annotations
import json
import logging
from collections.abc import Iterable, Iterator
from pathlib import Path
from openai import OpenAI
from pydantic import ValidationError
from .models import Finding
from .repo_map import ModuleNeighborhood, RepoMap
logger = logging.getLogger(__name__)
DEFAULT_SOURCE_EXTENSIONS: frozenset[str] = frozenset({".py"})
SKIP_DIR_NAMES: frozenset[str] = frozenset({
".git", ".venv", "venv", "env", "__pycache__", "node_modules",
"dist", "build", ".mypy_cache", ".pytest_cache", ".ruff_cache",
"site-packages",
})
MAX_FILE_BYTES = 200_000
def _load_prompt_template(name: str) -> str:
here = Path(__file__).resolve()
return (here.parents[2] / "prompts" / name).read_text(encoding="utf-8")
def iter_source_files(
root: Path, extensions: Iterable[str] = DEFAULT_SOURCE_EXTENSIONS
) -> Iterator[Path]:
exts = {e.lower() for e in extensions}
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
if path.suffix.lower() not in exts:
continue
if any(part in SKIP_DIR_NAMES for part in path.parts):
continue
try:
if path.stat().st_size > MAX_FILE_BYTES:
logger.warning("skipping %s: exceeds %d bytes", path, MAX_FILE_BYTES)
continue
except OSError:
continue
yield path
```
`MAX_FILE_BYTES` is a safety cap. Beyond \~200 KB we skip rather than send a huge prompt that's likely to be both expensive and low quality.
The next piece is the prompt builder. The template uses `{filename}`, `{source}`, and `{neighborhood}` as placeholders; we use `str.replace` rather than `.format()` because the template contains JSON examples with literal braces:
```python theme={"system"}
def _render_neighborhood(neighborhood: ModuleNeighborhood | None) -> str:
if neighborhood is None:
return "null"
return neighborhood.model_dump_json(indent=2)
def _build_prompt(
template: str, *, filename: str, source: str, neighborhood: ModuleNeighborhood | None
) -> str:
return (
template.replace("{filename}", filename)
.replace("{source}", source)
.replace("{neighborhood}", _render_neighborhood(neighborhood))
)
```
Now the parser. We deserialise the JSON, validate each finding through Pydantic, and drop individual malformed findings rather than failing the whole file. One bad finding shouldn't lose us the good ones:
```python theme={"system"}
def _parse_findings(raw: str, *, source_file: Path) -> list[Finding]:
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"model did not return valid JSON: {exc}") from exc
if not isinstance(data, dict) or "findings" not in data:
raise ValueError("model JSON missing 'findings' key")
findings: list[Finding] = []
for entry in data["findings"]:
try:
findings.append(Finding.model_validate(entry))
except ValidationError as exc:
logger.warning("dropping malformed finding from %s: %s", source_file, exc)
return findings
```
The Scanner emits IDs like `F-001` per file, but the Chainer needs to reference findings across the whole repo. We re-issue the IDs against a monotonic counter so they're globally unique:
```python theme={"system"}
def _renumber_findings(findings: list[Finding], offset: int) -> tuple[list[Finding], int]:
renumbered: list[Finding] = []
for i, f in enumerate(findings):
new_id = f"F-{offset + i + 1:03d}"
renumbered.append(f.model_copy(update={"id": new_id}))
return renumbered, offset + len(findings)
```
The single-file scan call combines all of this. We read the file, build the prompt, send it to Venice with `response_format={"type": "json_object"}` and a low temperature, and parse the result:
```python theme={"system"}
def scan_file(
client: OpenAI,
model: str,
path: Path,
*,
prompt_template: str,
repo_root: Path,
repo_map: RepoMap,
max_retries: int = 1,
) -> list[Finding]:
try:
source = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
logger.warning("could not read %s: %s", path, exc)
return []
rel = path.relative_to(repo_root)
neighborhood = repo_map.neighborhood(rel)
prompt = _build_prompt(
prompt_template, filename=str(rel), source=source, neighborhood=neighborhood
)
last_error: Exception | None = None
for attempt in range(max_retries + 1):
try:
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": (
"You are a precise static security analyst. You respond "
"only with valid JSON matching the schema in the user prompt."
),
},
{"role": "user", "content": prompt},
],
response_format={"type": "json_object"},
temperature=0.1,
)
except Exception as exc:
logger.warning("Venice call failed for %s on attempt %d: %s", rel, attempt, exc)
last_error = exc
continue
content = response.choices[0].message.content or ""
try:
findings = _parse_findings(content, source_file=path)
except ValueError as exc:
logger.warning("parse failure for %s on attempt %d: %s", rel, attempt, exc)
last_error = exc
continue
return [
f.model_copy(update={"evidence": f.evidence.model_copy(update={"file": rel})})
for f in findings
]
logger.error("giving up on %s after %d attempts: %s", rel, max_retries + 1, last_error)
return []
```
Two details worth highlighting:
* We patch the evidence file path to be relative to `repo_root` *after* parsing, since the model echoes back whatever filename we gave it but we want a single canonical form throughout the report.
* `temperature=0.1` is intentionally low. We want the Scanner to be conservative and consistent across runs; creativity is the Chainer's job.
Finally, the orchestrator that scans every eligible file under the root:
```python theme={"system"}
def scan_path(
client: OpenAI,
model: str,
root: Path,
repo_map: RepoMap,
*,
extensions: Iterable[str] = DEFAULT_SOURCE_EXTENSIONS,
) -> list[Finding]:
template = _load_prompt_template("scanner.md")
all_findings: list[Finding] = []
offset = 0
for path in iter_source_files(root, extensions=extensions):
logger.info("scanning %s", path.relative_to(root))
findings = scan_file(
client, model, path,
prompt_template=template,
repo_root=root,
repo_map=repo_map,
)
renumbered, offset = _renumber_findings(findings, offset)
all_findings.extend(renumbered)
return all_findings
```
The repo map gets built once by the caller and reused for every file, so the Scanner sees a consistent global structure even when individual files fail to parse or get skipped.
## Writing the Chainer Agent
The Chainer takes the union of Scanner findings plus the condensed repo map and asks Venice whether any of the findings combine into a real exploit chain. Two deterministic guardrails sit between the LLM and the report:
1. Every chain must reference only finding IDs the Scanner produced.
2. Every chain must claim only files that at least one referenced finding's evidence touches.
Chains that violate either rule get dropped at parse time. This stops the model from hallucinating chains "just in case" and from claiming a chain spans files it has no evidence for.
The Chainer prompt lives at `prompts/chainer.md`. The core of it looks like this:
````markdown theme={"system"}
You are a senior offensive security engineer. You are given a list of atomic
vulnerability findings discovered in a single codebase, plus a structural map
of that codebase showing every module's public symbols and import edges. Your
job is to identify whether any subset of the findings can be combined into a
real, end-to-end exploit chain.
# Rules
1. Output a single JSON object. No prose before or after. No markdown fences.
2. The object must match this schema exactly:
```json
{
"chains": [
{
"id": "C-001",
"findings": ["F-001", "F-003"],
"narrative": "Step-by-step explanation of how an attacker combines these specific findings into a single exploit. Reference each finding by ID where it is used.",
"severity": "high | critical",
"files_involved": ["pkg/validators.py", "pkg/fetcher.py"]
}
]
}
```
3. Chain IDs must be sequential: C-001, C-002, C-003, etc.
4. Every entry in `findings` MUST be the ID of a finding from the input list. You may NOT invent new finding IDs.
5. Every entry in `files_involved` MUST be the `evidence.file` of at least one of the findings you reference in this chain.
6. A chain must reference at least two distinct findings.
7. Chains are by definition severity high or critical. If a combination doesn't raise the impact above the highest individual severity, it is not a chain worth reporting.
8. If no real chain exists, return `{"chains": []}`. It is correct and expected for many codebases to have findings that do not chain.
````
The full prompt in the [reference repo](https://github.com/joshua-mo-143/venice-security-agent-demo/blob/main/prompts/chainer.md) also explains how to read the repo map, how to decide what goes in `files_involved`, and crucially, when *not* to chain. Telling the model "it is correct and expected for many codebases to have findings that do not chain" is what stops it from inventing chains to look productive.
Now the agent code. Create `src/venice_security_reviewer/chainer.py`:
```python theme={"system"}
from __future__ import annotations
import json
import logging
from pathlib import Path
from openai import OpenAI
from pydantic import ValidationError
from .models import Chain, Finding, validate_chain_references
from .repo_map import RepoMap
logger = logging.getLogger(__name__)
MAX_REPO_MAP_CHARS = 8000
def _load_prompt_template(name: str) -> str:
here = Path(__file__).resolve()
return (here.parents[2] / "prompts" / name).read_text(encoding="utf-8")
```
`MAX_REPO_MAP_CHARS = 8000` is a soft ceiling for the JSON-rendered repo map block in the Chainer prompt. At roughly 4 chars per token, that's \~2000 tokens, which sits comfortably inside any Venice model's context window even with findings and the narrative budget on top.
We serialise findings into a compact JSON block. Note we strip the `snippet` from evidence here on purpose: the Chainer doesn't need raw bytes to decide whether two findings combine, and including them roughly doubles the token cost on real codebases:
```python theme={"system"}
def _findings_to_input_json(findings: list[Finding]) -> str:
payload = [
{
"id": f.id,
"title": f.title,
"severity": f.severity,
"description": f.description,
"cwe": f.cwe,
"evidence": {
"file": str(f.evidence.file),
"start_line": f.evidence.start_line,
"end_line": f.evidence.end_line,
},
}
for f in findings
]
return json.dumps(payload, indent=2)
```
For larger codebases the full condensed repo map can blow past our character budget. When that happens, we prune to finding-bearing modules plus their direct neighbours. That preserves enough structure for the Chainer to reason about chains we have evidence for, and discards the rest:
```python theme={"system"}
def _prune_for_budget(
repo_map: RepoMap, findings: list[Finding], *, char_budget: int
) -> dict[str, object]:
full = repo_map.condensed_dict()
if len(json.dumps(full)) <= char_budget:
return full
finding_files = {f.evidence.file for f in findings}
keep_modules = {
m.module_name for m in repo_map.modules if m.path in finding_files
}
if not keep_modules:
return full
neighbours: set[str] = set()
for m in repo_map.modules:
if m.module_name in keep_modules:
for edge in m.imports:
neighbours.add(edge.from_module)
for edge in m.imports:
if edge.from_module in keep_modules:
neighbours.add(m.module_name)
keep_modules.update(neighbours)
pruned_modules = [
{
"path": str(m.path),
"module": m.module_name,
"exports": list(m.exports),
"imports": [
{"from": e.from_module, "names": list(e.imported_names)}
for e in m.imports
],
}
for m in repo_map.modules
if m.module_name in keep_modules
]
return {
"modules": pruned_modules,
"_pruned": True,
"_kept": len(pruned_modules),
"_total": len(repo_map.modules),
}
def _render_repo_map(
repo_map: RepoMap, findings: list[Finding], *, char_budget: int = MAX_REPO_MAP_CHARS
) -> str:
payload = _prune_for_budget(repo_map, findings, char_budget=char_budget)
if payload.get("_pruned"):
logger.info(
"chainer: repo map pruned for token budget (kept %s of %s modules)",
payload.get("_kept"),
payload.get("_total"),
)
return json.dumps(payload, indent=2)
```
The pruning strategy is intentionally simple: keep the modules our findings live in, and keep their direct import-graph neighbours. Anything further out has no plausible role in a chain we currently have evidence for, so it can be dropped without losing reasoning power. We also annotate the payload with `_pruned`, `_kept`, and `_total` markers, so the Chainer prompt can warn the model when the map has been trimmed.
Parsing the response is the same shape as the Scanner: deserialise, validate each chain through Pydantic, drop malformed entries:
```python theme={"system"}
def _parse_chains(raw: str) -> list[Chain]:
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"chainer did not return valid JSON: {exc}") from exc
if not isinstance(data, dict) or "chains" not in data:
raise ValueError("chainer JSON missing 'chains' key")
chains: list[Chain] = []
for entry in data["chains"]:
try:
chains.append(Chain.model_validate(entry))
except ValidationError as exc:
logger.warning("dropping malformed chain: %s", exc)
return chains
```
Then the agent itself:
```python theme={"system"}
def find_chains(
client: OpenAI,
model: str,
findings: list[Finding],
repo_map: RepoMap,
*,
max_retries: int = 1,
) -> tuple[list[Chain], list[Chain]]:
if len(findings) < 2:
return [], []
template = _load_prompt_template("chainer.md")
prompt = template.replace(
"{findings_json}", _findings_to_input_json(findings)
).replace("{repo_map}", _render_repo_map(repo_map, findings))
last_error: Exception | None = None
for attempt in range(max_retries + 1):
try:
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": (
"You are a senior offensive security engineer. You respond "
"only with valid JSON matching the schema in the user prompt."
),
},
{"role": "user", "content": prompt},
],
response_format={"type": "json_object"},
temperature=0.2,
)
except Exception as exc:
logger.warning("Venice call failed on attempt %d: %s", attempt, exc)
last_error = exc
continue
content = response.choices[0].message.content or ""
try:
chains = _parse_chains(content)
except ValueError as exc:
logger.warning("chainer parse failure on attempt %d: %s", attempt, exc)
last_error = exc
continue
valid, dropped = validate_chain_references(chains, findings)
if dropped:
logger.warning(
"chainer referenced %d unknown finding id(s) or file(s); chains dropped: %s",
len(dropped),
[c.id for c in dropped],
)
return valid, dropped
logger.error("giving up on chainer after %d attempts: %s", max_retries + 1, last_error)
return [], []
```
A couple of things worth pointing out:
* We bail out before calling the model when there are fewer than two findings. You can't chain a single finding, and skipping the call means we don't burn tokens on a guaranteed-empty result.
* `temperature=0.2` is slightly higher than the Scanner's `0.1`. The Chainer benefits from a touch more creativity to spot non-obvious combinations, but we still want it grounded in the findings and map it was given.
* After parsing, `validate_chain_references` runs the deterministic cross-reference check we wrote earlier. Anything that survives is safe to render; anything that doesn't gets logged so the operator knows the model tried to invent something.
That cross-reference check is the most important piece of the whole project. It's the boundary between "useful security tool" and "occasionally confidently wrong AI report." With it in place, even if the model hallucinates, the wrong chain never reaches the report.
## Rendering the Markdown Report
Keeping rendering separate from agent logic means the same `Finding` and `Chain` objects can later be fed into other formats (JSON, SARIF, HTML) without touching the Scanner or Chainer.
We'll use Jinja2 with a small template file. Create `src/venice_security_reviewer/templates/report.md.j2`:
````jinja theme={"system"}
# Security Review Report
**Target:** `{{ target }}`
**Scanned at:** {{ scanned_at }}
**Model:** `{{ model }}`
---
## Summary
- **Atomic findings:** {{ findings | length }}
- **Exploit chains:** {{ chains | length }}
{%- if dropped_chains %}
- **Dropped chains (referenced unknown findings):** {{ dropped_chains | length }}
{%- endif %}
---
## Exploit Chains
{% if not chains %}
_No exploit chains were identified by the Chainer agent._
{% else %}
{% for c in chains %}
### {{ c.id }} — {{ c.severity | upper }}
**Findings combined:** {{ c.findings | join(', ') }}
**Files involved:** {{ c.files_involved | map('string') | join(', ') }}
{{ c.narrative }}
{% endfor %}
{% endif %}
---
## Atomic Findings
{% for f in findings %}
### {{ f.id }} — {{ f.title }}
- **Severity:** {{ f.severity }}
{%- if f.cwe %}
- **CWE:** {{ f.cwe }}
{%- endif %}
- **Location:** `{{ f.evidence.file }}:{{ f.evidence.start_line }}-{{ f.evidence.end_line }}`
{{ f.description }}
```
{{ f.evidence.snippet }}
```
{% endfor %}
````
Then the renderer at `src/venice_security_reviewer/report.py`:
```python theme={"system"}
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from jinja2 import Environment, PackageLoader, select_autoescape
from .models import Chain, Finding
def _build_env() -> Environment:
return Environment(
loader=PackageLoader("venice_security_reviewer", "templates"),
autoescape=select_autoescape(enabled_extensions=("html",)),
keep_trailing_newline=True,
)
def render_report(
*,
target: Path,
model: str,
findings: list[Finding],
chains: list[Chain],
dropped_chains: list[Chain] | None = None,
) -> str:
env = _build_env()
template = env.get_template("report.md.j2")
return template.render(
target=str(target),
scanned_at=datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
model=model,
findings=findings,
chains=chains,
dropped_chains=dropped_chains or [],
)
```
Autoescape stays off for the Markdown template (Markdown isn't HTML), but we leave it enabled for any future `.html` templates by extension.
## Wiring the CLI
The CLI is the orchestrator: build the repo map, scan, chain, render. We'll use Typer to handle argument parsing and Rich to print a nice summary table.
Create `src/venice_security_reviewer/cli.py`:
```python theme={"system"}
from __future__ import annotations
import logging
import sys
from pathlib import Path
from typing import Annotated
import typer
from rich.console import Console
from rich.table import Table
from .chainer import find_chains
from .client import VeniceConfigError, build_client
from .models import Chain, Finding
from .repo_map import build_repo_map
from .report import render_report
from .scanner import scan_path
app = typer.Typer(
add_completion=False,
help="Two-agent security code reviewer powered by Venice AI.",
no_args_is_help=True,
)
console = Console()
@app.callback()
def _root() -> None:
"""Force Typer to keep `scan` as a named subcommand."""
def _configure_logging(verbose: bool) -> None:
logging.basicConfig(
level=logging.DEBUG if verbose else logging.INFO,
format="%(levelname)s %(name)s: %(message)s",
stream=sys.stderr,
)
def _print_summary(
findings: list[Finding], chains: list[Chain], dropped: list[Chain]
) -> None:
table = Table(title="Scan summary", show_header=True, header_style="bold")
table.add_column("Metric")
table.add_column("Count", justify="right")
table.add_row("Atomic findings", str(len(findings)))
table.add_row("Exploit chains", str(len(chains)))
if dropped:
table.add_row("Chains dropped (bad refs)", str(len(dropped)))
console.print(table)
@app.command()
def scan(
path: Annotated[
Path,
typer.Argument(
exists=True, file_okay=False, dir_okay=True, readable=True, resolve_path=True,
help="Path to the codebase to scan.",
),
],
out: Annotated[
Path, typer.Option("--out", "-o", help="Where to write the Markdown report.")
] = Path("report.md"),
verbose: Annotated[
bool, typer.Option("--verbose", "-v", help="Enable debug logging.")
] = False,
) -> None:
"""Scan a codebase for vulnerabilities and exploit chains."""
_configure_logging(verbose)
try:
client, model = build_client()
except VeniceConfigError as exc:
console.print(f"[red]error:[/red] {exc}")
raise typer.Exit(code=2) from exc
console.print(f"[bold]Indexing[/bold] {path} (AST repo map)...")
repo_map = build_repo_map(path)
edge_count = sum(len(m.imports) for m in repo_map.modules)
console.print(
f"Repo map: [bold]{len(repo_map.modules)}[/bold] module(s), "
f"[bold]{edge_count}[/bold] import edge(s)."
)
console.print(f"[bold]Scanning[/bold] {path} with model [cyan]{model}[/cyan]...")
findings = scan_path(client, model, path, repo_map)
console.print(f"Scanner produced [bold]{len(findings)}[/bold] finding(s).")
console.print("[bold]Chaining[/bold] findings...")
chains, dropped = find_chains(client, model, findings, repo_map)
console.print(f"Chainer produced [bold]{len(chains)}[/bold] chain(s).")
report = render_report(
target=path, model=model,
findings=findings, chains=chains, dropped_chains=dropped,
)
out.write_text(report, encoding="utf-8")
console.print(f"Report written to [green]{out}[/green]")
_print_summary(findings, chains, dropped)
def main() -> None:
app()
if __name__ == "__main__":
main()
```
Add the script entry point to `pyproject.toml`:
```toml theme={"system"}
[project.scripts]
venice-security-reviewer = "venice_security_reviewer.cli:app"
```
That's the whole pipeline wired up.
## Testing the Guardrails
We've leaned hard on one idea throughout this build: the deterministic guardrails are what separate a useful security tool from a confidently wrong one. That claim is only worth making if we can prove the guardrails actually hold, so the most valuable tests in this project don't call Venice at all. They lock down the Pydantic boundary and the prompt-assembly plumbing, which means they run offline, in milliseconds, with no API key and no token cost.
Add the dev dependencies first:
```bash theme={"system"}
uv add --dev "pytest>=8.3" "ruff>=0.7" "mypy>=1.13"
```
The first thing worth testing is the model boundary itself. These tests assert that malformed findings and chains are rejected at construction time, before they can ever reach a report. Create `tests/test_models.py`:
```python theme={"system"}
from __future__ import annotations
from pathlib import Path
import pytest
from pydantic import ValidationError
from venice_security_reviewer.models import (
Chain,
Evidence,
Finding,
validate_chain_references,
)
def _finding(fid: str) -> Finding:
return Finding(
id=fid,
title="t",
severity="medium",
description="d",
evidence=Evidence(file=Path("a.py"), start_line=1, end_line=2, snippet="x"),
)
def test_evidence_rejects_inverted_line_range() -> None:
with pytest.raises(ValidationError):
Evidence(file=Path("a.py"), start_line=10, end_line=5, snippet="x")
def test_finding_id_pattern_enforced() -> None:
with pytest.raises(ValidationError):
Finding(
id="not-an-id",
title="t",
severity="medium",
description="d",
evidence=Evidence(file=Path("a.py"), start_line=1, end_line=2, snippet="x"),
)
def test_chain_requires_two_findings() -> None:
with pytest.raises(ValidationError):
Chain(
id="C-001",
findings=["F-001"],
narrative="n",
severity="high",
files_involved=[Path("a.py")],
)
```
Each of these mirrors a constraint we put on the models earlier: an inverted line range, an ID that doesn't match the `F-###` pattern, and a "chain" of a single finding. If any of them ever stops raising, a whole class of hallucination has quietly become possible again.
The most important test covers the cross-reference validator, since that's the function that actually drops invented chains:
```python theme={"system"}
def test_validate_chain_references_drops_unknown_ids() -> None:
findings = [_finding("F-001"), _finding("F-002")]
good = Chain(
id="C-001",
findings=["F-001", "F-002"],
narrative="n",
severity="critical",
files_involved=[Path("a.py")],
)
bad = Chain(
id="C-002",
findings=["F-001", "F-999"],
narrative="n",
severity="critical",
files_involved=[Path("a.py")],
)
valid, dropped = validate_chain_references([good, bad], findings)
assert [c.id for c in valid] == ["C-001"]
assert [c.id for c in dropped] == ["C-002"]
```
`F-999` was never produced by the Scanner, so the chain that references it lands in `dropped` and never reaches the report. The companion test in the reference repo, `test_validate_chain_references_drops_unknown_files`, does the same for a chain that claims a file none of its findings came from.
The second thing worth testing is the plumbing that feeds the Chainer. It's easy to refactor the prompt assembly and silently stop passing cross-file context, at which point the Chainer would keep working but quietly get worse. This test builds a two-module fixture, renders the prompt, and asserts the cross-file information is actually present, again without a Venice round-trip. Create `tests/test_cross_file_chain.py`:
```python theme={"system"}
from __future__ import annotations
from pathlib import Path
from venice_security_reviewer.chainer import (
_findings_to_input_json,
_load_prompt_template,
_render_repo_map,
)
from venice_security_reviewer.models import Evidence, Finding
from venice_security_reviewer.repo_map import build_repo_map
def _write(root: Path, rel: str, content: str) -> None:
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def test_chainer_prompt_carries_cross_file_context(tmp_path: Path) -> None:
_write(tmp_path, "validators.py", "def is_safe_url(url: str) -> bool:\n return True")
_write(
tmp_path,
"fetcher.py",
"from .validators import is_safe_url\n\ndef fetch(url: str) -> bytes:\n return b''",
)
rmap = build_repo_map(tmp_path)
findings = [
Finding(
id="F-001",
title="Validator returns True unconditionally",
severity="low",
description="The validator always returns True.",
evidence=Evidence(
file=Path("validators.py"), start_line=1, end_line=2, snippet="..."
),
),
Finding(
id="F-002",
title="Fetcher trusts a stub validator",
severity="low",
description="The fetcher gates network access on is_safe_url.",
evidence=Evidence(
file=Path("fetcher.py"), start_line=1, end_line=1, snippet="..."
),
),
]
template = _load_prompt_template("chainer.md")
prompt = template.replace(
"{findings_json}", _findings_to_input_json(findings)
).replace("{repo_map}", _render_repo_map(rmap, findings))
assert "{findings_json}" not in prompt and "{repo_map}" not in prompt
assert "F-001" in prompt and "F-002" in prompt
assert "validators.py" in prompt and "fetcher.py" in prompt
assert "is_safe_url" in prompt
```
If this test passes, the Chainer is being handed a prompt that contains both findings, both file paths, and the import edge between them. Whether the *model* uses that information well is a separate, out-of-band evaluation; this test only guards the plumbing that gets the information into the prompt in the first place.
Run the whole suite, plus the linter and type checker, with:
```bash theme={"system"}
uv run pytest # offline tests, no live Venice calls
uv run ruff check .
uv run mypy src/
```
Because none of these tests touch the network, they're safe to run on every commit and in CI without burning tokens or needing a Venice key. The reference repo also includes `tests/test_scanner_parse.py`, `tests/test_chainer_parse.py`, and `tests/test_repo_map.py`, which cover JSON parsing edge cases (malformed entries getting dropped rather than crashing the run) and the AST repo map builder.
## Running the Project
To try it on a real codebase, point the CLI at a directory of Python source:
```bash theme={"system"}
uv run venice-security-reviewer scan path/to/your/code
```
Or install it into your virtualenv with `pip install -e .` and run `venice-security-reviewer scan path/to/your/code`.
The output looks roughly like this:
```text theme={"system"}
Indexing /path/to/code (AST repo map)...
Repo map: 6 module(s), 14 import edge(s).
Scanning /path/to/code with model zai-org-glm-5...
Scanner produced 4 finding(s).
Chaining findings...
Chainer produced 1 chain(s).
Report written to report.md
Scan summary
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Metric ┃ Count ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ Atomic findings │ 4 │
│ Exploit chains │ 1 │
└───────────────────────────┴───────┘
```
The Markdown report shows each chain at the top with its narrative, then every individual finding underneath with severity, CWE, file location, description, and the verbatim snippet the model claims to have read.
The reference repo also ships with four bundled demo targets that each exercise a different shape of reasoning the Chainer has to do:
* `examples/vulnerable_app` — a multi-file Flask app with three "low" findings, two of which combine into a critical privilege-escalation chain across files. Tests whether the Chainer is selective about what it combines.
* `examples/url_preview` — a multi-file URL-fetcher with a defensive allowlist that doesn't apply per-iteration. Tests cross-file data-flow reasoning combined with deployment topology (link-local IPs are cloud-credential gateways).
* `examples/csv_query` — a single-file CSV filter with an `eval` sandbox escape via `__class__.__base__.__subclasses__()`. Tests language-level reasoning rather than HTTP flow.
* `examples/webhook_handler` — a single-file HMAC verifier with a JSON parser-differential vulnerability. Tests reasoning across multiple specifications.
Try them with:
```bash theme={"system"}
uv run venice-security-reviewer scan examples/vulnerable_app
uv run venice-security-reviewer scan examples/csv_query
```
If you ever see the CLI log `chainer referenced N unknown finding id(s) or file(s); chains dropped`, that's the cross-reference validator catching the model in the act of inventing a chain. The dropped chains never make it into the report; you just get a warning that you can use to adjust the prompt or sample additional Chainer runs.
## Extending This Example
The two-agent shape generalises well. A few directions worth exploring:
* **More languages.** The Scanner is language-agnostic at the prompt level; the AST builder is what's Python-specific. Swap in `tree-sitter` and you can build the same neighbourhood/condensed-map shapes for TypeScript, Go, or Rust.
* **A third agent for fixes.** Once you have a chain, asking a Patcher agent to draft a unified diff that defangs *one* of the constituent findings is a small step. Pydantic-validate the diff against the same evidence-file set and you get the same hallucination guard for free.
* **Output formats.** `render_report` is the only place that knows about Markdown. Add a SARIF renderer and the same findings can drop into GitHub code scanning. Add a JSON renderer and you can pipe results into a downstream system.
* **Caching by file hash.** The Scanner's per-file calls are independent and idempotent. Caching by `(file_hash, prompt_hash, model)` means re-scanning a repo where one file changed only re-runs the Scanner on that one file.
* **Sampling for the Chainer.** For high-stakes runs, call the Chainer N times at slightly higher temperature and intersect the results. Chains the model finds consistently are more likely to be real; chains it finds once and never again are likely noise.
* **Stronger models.** `zai-org-glm-5` is the default because it strikes a good balance between cost and quality for combinatorial reasoning, but for harder codebases swapping in a stronger Venice model (set via `VENICE_MODEL`) can make the Chainer's narratives noticeably tighter.
## Finishing Up
Thanks for reading! Hopefully this helped you understand how to structure an AI security tool that's actually trustworthy. The pattern we used here generalises beyond security too: any time you want an LLM to reason across files in a way that has to ground out in real evidence, the recipe is the same. Build a deterministic structural map, hand the model a slice of it that fits in context, validate the model's references back against the structure, and drop anything it can't ground.
By using Python with the Venice AI API, we can build agents that combine LLM reasoning with hard validation boundaries, and ship something that gives a useful answer instead of a confident-sounding one.
# Embedding Models
Source: https://docs.venice.ai/models/embeddings
Venice embedding models for semantic search, RAG retrieval, and clustering, including pricing, dimensions, and OpenAI-compatible /embeddings usage.
Loading models...
***
See the [Embeddings API](/api-reference/endpoint/embeddings/generate) for usage examples.
# Image Models
Source: https://docs.venice.ai/models/image
Venice image models for text-to-image generation, image editing, background removal, and upscaling, with model IDs, pricing, and supported parameters.
Loading models...
***
## Model Types
* **Generation:** Create images from text prompts
* **Upscale:** Enhance image resolution and quality
* **Edit:** Modify existing images with inpainting
See the [Image Generate API](/api-reference/endpoint/image/generate) for text-to-image, [Upscale API](/api-reference/endpoint/image/upscale) for enhancement, and [Edit API](/api-reference/endpoint/image/edit) for inpainting.
Image generation sizing is model-specific. Pixel-based models such as `venice-sd35` and `qwen-image` use `width` and `height`; aspect-ratio models such as `qwen-image-2` use `aspect_ratio`; models that list resolution options such as `1K`, `2K`, or `4K` use both `resolution` and `aspect_ratio`.
**Quality tiers.** `gpt-image-2` (generate) and `gpt-image-2-edit` (edit/multi-edit) accept an optional `quality` parameter — `low`, `medium`, or `high` (default). Lower tiers cost less; see the [Pricing overview](/overview/pricing#quality-tier-pricing-gpt-image-2) for the full matrix or fetch `model_spec.pricing.quality` from the [Models endpoint](/api-reference/endpoint/models/list).
# Music & Sound Effects Models
Source: https://docs.venice.ai/models/music
Venice music and audio models for AI-generated songs, instrumental tracks, and sound effects synthesis, with model IDs, pricing, and prompt guidance.
Loading models...
## Model Categories
**Song Generation:** Create full songs with optional lyrics and vocal support
* ACE-Step 1.5, ElevenLabs Music, MiniMax Music 2.0
**Music & Sound Effects:** Generate instrumental music or sound effects from text prompts
* Stable Audio 2.5
**Sound Effects:** Synthesize audio effects and ambient sounds from text prompts
* ElevenLabs Sound Effects, MMAudio V2
ElevenLabs Music is the only model that supports `force_instrumental` to generate music without vocals.
Audio generation uses an async queue system. See the [Audio Queue API](/api-reference/endpoint/audio/queue) to start generation and [Audio Retrieve API](/api-reference/endpoint/audio/retrieve) to fetch results.
## Pricing
Pricing varies by model:
* **Per-generation:** Fixed price per audio clip (MiniMax Music 2.0, Stable Audio 2.5)
* **Duration-tiered:** Price scales with duration tier (ElevenLabs Music, ACE-Step 1.5)
* **Per-second:** Price based on output duration (ElevenLabs Sound Effects, MMAudio V2)
For exact quotes before generation, use the [Audio Quote API](/api-reference/endpoint/audio/quote).
### Duration-Tiered Pricing
Models with duration-tiered pricing accept any `duration_seconds` within the model's `min_duration`–`max_duration` range. The price is determined by which tier the requested duration falls into. Tier ranges are returned in the `/models` response under `pricing.durations`, with `min_seconds` and `max_seconds` for each tier.
For example, ElevenLabs Music accepts 3–600 seconds (up to 10 minutes) at \$0.75 per minute, rounded up to the nearest minute:
| Duration Range | Tier Key | Base Price |
| -------------- | -------- | ---------- |
| 3–60s | `60` | \$0.75 |
| 61–120s | `120` | \$1.50 |
| 121–180s | `180` | \$2.25 |
| 181–240s | `240` | \$3.00 |
| 241–300s | `300` | \$3.75 |
| 301–360s | `360` | \$4.50 |
| 361–420s | `420` | \$5.25 |
| 421–480s | `480` | \$6.00 |
| 481–540s | `540` | \$6.75 |
| 541–600s | `600` | \$7.50 |
These are base prices before markup. Use the [Audio Quote API](/api-reference/endpoint/audio/quote) to get the exact price you will be charged.
## Key Parameters
| Parameter | Description |
| -------------------- | ----------------------------------------------------------------------------- |
| `prompt` | Text description of the audio to generate |
| `lyrics_prompt` | Song lyrics for vocal models (required when model has `lyrics_required=true`) |
| `duration_seconds` | Output length in seconds |
| `force_instrumental` | Generate without vocals (where supported) |
# All Models
Source: https://docs.venice.ai/models/overview
Catalog of all models available on the Venice API across text, image, video, audio, embeddings, and speech, with capabilities, pricing, and model IDs.
Loading models...
# Speech-to-Text Models
Source: https://docs.venice.ai/models/speech-to-text
Venice speech-to-text models for transcribing audio to text with multilingual support, timestamps, and an OpenAI-compatible /audio/transcriptions endpoint.
Loading models...
***
## Usage
Speech-to-text models transcribe spoken audio into written text. They are accessed via the [Audio Transcriptions API](/api-reference/endpoint/audio/transcriptions).
### Supported audio formats
`mp3`, `mp4`, `mpeg`, `mpga`, `m4a`, `wav`, `webm`, `flac`, `ogg`
### Response formats
| Format | Description |
| -------------- | --------------------------------------------------------- |
| `json` | Default. Returns `{ "text": "..." }`. |
| `text` | Plain transcribed text. |
| `srt` | SubRip subtitle format with timestamps. |
| `vtt` | WebVTT subtitle format with timestamps. |
| `verbose_json` | Full response with segment-level timestamps and metadata. |
Pricing is billed per second of input audio. See the [Audio Transcriptions API](/api-reference/endpoint/audio/transcriptions) for request examples and parameter details.
# Text Models
Source: https://docs.venice.ai/models/text
Venice chat, reasoning, and code-generation text models with context lengths, pricing, traits, and OpenAI-compatible /chat/completions usage examples.
Loading models...
***
## Capabilities
* **Function Calling:** Let the model invoke tools and external APIs
* **Reasoning:** Extended thinking for complex problem-solving
* **Vision:** Analyze images alongside text prompts
* **Code:** Optimized for code generation and understanding
See the [Chat Completions API](/api-reference/endpoint/chat/completions) for usage examples.
# Text-to-Speech Models
Source: https://docs.venice.ai/models/text-to-speech
Venice text-to-speech models with multilingual voices, voice selection, and streaming audio output via the OpenAI-compatible /audio/speech endpoint.
Loading models...
***
## Voice catalog
Voices are **model-specific**. The `voice` you pass must come from the catalog
of the `model` you selected. Pick a model below to browse its voices.
Loading voices...
Voice IDs are case-sensitive and **only valid for the matching `model`**. Pass
both fields together in your request payload. See the
[Audio Speech API](/api-reference/endpoint/audio/speech) for examples.
### Example request
```bash theme={"system"}
curl https://api.venice.ai/api/v1/audio/speech \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "tts-kokoro",
"voice": "af_sky",
"input": "Hello from Venice."
}' \
--output speech.mp3
```
To switch models, change **both** `model` and `voice` to a pair from the
selected model above.
# Video Models
Source: https://docs.venice.ai/models/video
Venice video models for text-to-video, image-to-video, reference-to-video, and Topaz upscaling, with model IDs, pricing, and supported resolutions.
Loading models...
## Model Types
**Text to Video:** Generate videos from text prompts
**Image to Video:** Animate static images into video clips
**Video Upscaling:** Enhance existing videos to higher resolutions using AI-powered upscaling. See the [Video Upscaling Guide](/guides/media/video-upscaling) for details.
Video generation and upscaling use an async queue system. See the [Video Queue API](/api-reference/endpoint/video/queue) to start generation and [Video Retrieve API](/api-reference/endpoint/video/retrieve) to fetch results.
## Pricing
Adjust the dropdowns to see how duration, resolution, and audio affect the price. Models marked **FIXED** have a flat rate.
For exact quotes before generation, use the [Video Quote API](/api-reference/endpoint/video/quote).
# Venice API
Source: https://docs.venice.ai/overview/about-venice
Venice API documentation — private, unrestricted access to OpenAI-compatible chat, image, audio, and video models behind one API key.
The API for private, unrestricted access to intelligence.
OpenAI-compatible chat, image, audio, and video behind one API key.
```bash curl theme={"system"}
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "zai-org-glm-5-1",
"messages": [{"role": "user", "content": "Build without permission."}]
}'
```
```ts TypeScript theme={"system"}
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.VENICE_API_KEY,
baseURL: "https://api.venice.ai/api/v1",
});
const res = await client.chat.completions.create({
model: "zai-org-glm-5-1",
messages: [{ role: "user", content: "Build without permission." }],
});
```
```python Python theme={"system"}
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["VENICE_API_KEY"],
base_url="https://api.venice.ai/api/v1",
)
res = client.chat.completions.create(
model="zai-org-glm-5-1",
messages=[{"role": "user", "content": "Build without permission."}],
)
```
Endpoints
One API for every modality
Chat, image, audio, video, and embeddings behind one API key.
Chat Completions
OpenAI-compatible chat with reasoning, tool use, and streaming across 100+ text models.
Streaming
Tools
Vision
See reference →
Image Generation
Text-to-image, image-to-image, upscaling, and background removal across photorealistic, stylized, and uncensored models.
Text-to-image
Image-to-image
Upscale
See reference →
Audio
Text-to-speech with 50+ multilingual voices, plus speech-to-text transcription for any audio file.
TTS
Transcription
50+ voices
See reference →
Video
Text-to-video, image-to-video, and reference-to-video through a sync or async job queue.
Text-to-video
Image-to-video
Reference-to-video
See reference →
Plus embeddings, file inputs, MCP tools, and wallet payments. View all endpoints →
Agents
Built for AI agents
Private inference, MCP tools, and wallet-funded workflows for messaging, coding, and onchain agents.
Agent apps
Connect Venice to WhatsApp, Telegram, Discord, and more through OpenClaw, Hermes, and NanoClaw.
See integrations →
Coding agents
Use Claude Code, Cursor, and Codex CLI with Venice models for private coding workflows.
See integrations →
MCP + Skills
Expose chat, image, video, audio, and embeddings as MCP tools or runtime skills.
See integrations →
Explore the AI Agents hub →
Models
Popular models
A few of the most-used models on Venice. Use the ID as your `model` parameter.
250+ models
Text, image, audio, and video
Browse the catalog →
Tools
Built‑in tools for chat models
Turn on web search, attach files, or query a blockchain with `venice_parameters` or a Venice-native endpoint.
Add real-time web search with citations to any text model via `enable_web_search`.
```bash Curl theme={"system"}
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "zai-org-glm-5-1",
"messages": [{"role": "user", "content": "What are the latest developments in AI?"}],
"venice_parameters": {
"enable_web_search": "auto"
}
}'
```
```ts TypeScript theme={"system"}
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.VENICE_API_KEY!,
baseURL: "https://api.venice.ai/api/v1",
});
const completion = await client.chat.completions.create({
model: "zai-org-glm-5-1",
messages: [{ role: "user", content: "What are the latest developments in AI?" }],
// @ts-expect-error - Venice-specific parameter
venice_parameters: {
enable_web_search: "auto",
},
});
console.log(completion.choices[0].message.content);
```
```python Python theme={"system"}
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["VENICE_API_KEY"],
base_url="https://api.venice.ai/api/v1",
)
response = client.chat.completions.create(
model="zai-org-glm-5-1",
messages=[{"role": "user", "content": "What are the latest developments in AI?"}],
extra_body={
"venice_parameters": {
"enable_web_search": "auto",
}
},
)
print(response.choices[0].message.content)
```
```bash Model Suffix theme={"system"}
# Alternative: append parameters directly to the model ID
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "zai-org-glm-5-1:enable_web_search=on&enable_web_citations=true",
"messages": [{"role": "user", "content": "What are the latest developments in AI?"}]
}'
```
Set `enable_web_scraping: true` and the model will fetch and read any URLs in the user message before answering.
```bash Curl theme={"system"}
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai-gpt-55",
"messages": [
{"role": "user", "content": "Summarize this post in five bullets: https://venice.ai/blog/how-to-use-venice-api"}
],
"venice_parameters": {
"enable_web_scraping": true
}
}'
```
```ts TypeScript theme={"system"}
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.VENICE_API_KEY!,
baseURL: "https://api.venice.ai/api/v1",
});
const response = await client.chat.completions.create({
model: "openai-gpt-55",
messages: [
{
role: "user",
content:
"Summarize this post in five bullets: https://venice.ai/blog/how-to-use-venice-api",
},
],
// @ts-expect-error - Venice-specific parameter
venice_parameters: {
enable_web_scraping: true,
},
});
console.log(response.choices[0].message.content);
```
```python Python theme={"system"}
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["VENICE_API_KEY"],
base_url="https://api.venice.ai/api/v1",
)
response = client.chat.completions.create(
model="openai-gpt-55",
messages=[
{
"role": "user",
"content": "Summarize this post in five bullets: https://venice.ai/blog/how-to-use-venice-api",
}
],
extra_body={
"venice_parameters": {
"enable_web_scraping": True,
}
},
)
print(response.choices[0].message.content)
```
Attach PDFs, Office docs, code, and text files (up to 25MB) directly to a chat request. See the [File Inputs guide](/guides/features/file-inputs) for the full format list.
```bash Curl theme={"system"}
# Encode a local file as a base64 data URL, then send it inline
FILE_B64=$(base64 q3-report.pdf | tr -d '\n')
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"openai-gpt-55\",
\"messages\": [
{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"Summarize this report in five bullets and list the main risks.\"},
{\"type\": \"file\", \"file\": {\"filename\": \"q3-report.pdf\", \"file_data\": \"data:application/pdf;base64,${FILE_B64}\"}}
]
}
]
}"
```
```ts TypeScript theme={"system"}
import OpenAI from "openai";
import { readFile } from "node:fs/promises";
const client = new OpenAI({
apiKey: process.env.VENICE_API_KEY!,
baseURL: "https://api.venice.ai/api/v1",
});
const pdf = await readFile("q3-report.pdf");
const fileData = `data:application/pdf;base64,${pdf.toString("base64")}`;
const response = await client.chat.completions.create({
model: "openai-gpt-55",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Summarize this report in five bullets and list the main risks." },
// @ts-expect-error - Venice file input block
{ type: "file", file: { filename: "q3-report.pdf", file_data: fileData } },
],
},
],
});
console.log(response.choices[0].message.content);
```
```python Python theme={"system"}
import base64
import os
from pathlib import Path
from openai import OpenAI
client = OpenAI(
api_key=os.environ["VENICE_API_KEY"],
base_url="https://api.venice.ai/api/v1",
)
path = Path("q3-report.pdf")
file_data = "data:application/pdf;base64," + base64.b64encode(path.read_bytes()).decode("utf-8")
response = client.chat.completions.create(
model="openai-gpt-55",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this report in five bullets and list the main risks."},
{"type": "file", "file": {"filename": "q3-report.pdf", "file_data": file_data}},
],
}
],
)
print(response.choices[0].message.content)
```
Proxy JSON-RPC 2.0 calls across 11 supported chains with your Venice key or an x402 wallet. See the [Crypto RPC reference](/api-reference/endpoint/crypto/rpc) for chains, methods, and credit tiers.
```bash Curl theme={"system"}
curl https://api.venice.ai/api/v1/crypto/rpc/ethereum-mainnet \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}'
```
```ts TypeScript theme={"system"}
const response = await fetch(
"https://api.venice.ai/api/v1/crypto/rpc/base-mainnet",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VENICE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify([
{ jsonrpc: "2.0", method: "eth_chainId", params: [], id: 1 },
{ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 2 },
]),
}
);
const results = await response.json();
console.log(results);
```
```python Python theme={"system"}
import os
import requests
response = requests.post(
"https://api.venice.ai/api/v1/crypto/rpc/ethereum-mainnet",
headers={
"Authorization": f"Bearer {os.environ['VENICE_API_KEY']}",
"Content-Type": "application/json",
},
json={
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest"],
"id": 1,
},
)
print(response.json())
```
Pricing
Top up, stake, or pay per request
Fund an account with credits, stake DIEM for a daily allowance, or skip the account entirely with USDC on Base.
Credits
USD or Crypto
Pay as you go in USD or crypto. Credits never expire and work across every endpoint.
Buy Credits
DIEM
Daily allowance
Stake DIEM or VVV once and earn a fixed inference allowance every day, with no per-call charges.
Learn about DIEM
x402
USDC on Base
Pay per request from any Base wallet in USDC. No account or API key, built for agents.
Read x402 Guide
Questions or feedback? Join us on [Discord](https://discord.gg/askvenice).
# Beta Models
Source: https://docs.venice.ai/overview/beta-models
Preview new Venice models in beta — try upcoming chat, image, video, and audio models for evaluation before they reach general availability on the API.
We sometimes release models in beta to gather feedback and confirm their performance before a full production rollout. Beta models are available to all users but are **not recommended for production use**.
Beta status does not guarantee promotion to production. A beta model may be removed if it is too costly to run, performs poorly at scale, or raises safety concerns. Beta models can change without notice and may have limited documentation or support. Models that prove stable, broadly useful, and aligned with our standards are promoted to general availability.
## Important Considerations
When using beta models, keep in mind:
* May be changed or removed at any time without the standard deprecation notice period
* Not suitable for production applications or critical workflows
* May have inconsistent performance, availability, or behavior
* Limited or no migration support if removed
* Best used for testing, evaluation, and experimental projects
For production applications, we recommend using the stable models from our [main model lineup](/models/overview).
## Current Beta Models
The following models are currently available in beta.
### Checking Beta Status via the API
You can check if a model is in beta by calling the [List Models](/api-reference/endpoint/models/list) endpoint. Beta models include a `betaModel` field set to `true` in their `model_spec`:
```json theme={"system"}
{
"id": "some-beta-model",
"model_spec": {
"name": "Some Beta Model",
"betaModel": true,
"privacy": "private"
},
"type": "text",
"object": "model",
"owned_by": "venice.ai"
}
```
You can check `if (model.model_spec.betaModel)` to identify beta models and warn users or handle them differently in your application.
## Join the Alpha Testing Program
Want to help shape Venice's future models and features? Join our alpha testing program to get early access to new models before they're released publicly, provide feedback that influences development, and help us validate performance at scale.
[Learn how to join the alpha testing group](https://venice.ai/faqs#how-do-i-join-the-beta-testing-group)
# Deprecations
Source: https://docs.venice.ai/overview/deprecations
Venice API model lifecycle policy, deprecation timelines, and the list of retired or sunset models so you can plan upgrades before models are removed.
The Venice API exists to give developers unrestricted private access to production-grade models free from hidden filters or black-box decisions.
As models improve, we occasionally retire older ones in favor of smarter, faster, or more capable alternatives. We design these transitions to be predictable and low‑friction.
## Model Deprecations
We know deprecations can be disruptive. That’s why we aim to deprecate only when necessary, and we design features like traits and Venice-branded models to minimize disruption.
We may deprecate a model when:
* A newer model offers a clear improvement for the same use case
* The model no longer meets our standards for performance or reliability
* It sees consistently low usage, and continuing to support it would fragment the experience for everyone else
## Deprecation Process
When a model meets deprecation criteria, we provide 7–15 days' notice before removal. You can track upcoming deprecations through:
* The [Model Deprecation Tracker](#model-deprecation-tracker) below
* The [List Models](/api-reference/endpoint/models/list) API endpoint (models with a scheduled retirement include a `deprecation` object)
* Our [changelog](https://featurebase.venice.ai/changelog)
* Email notifications, sent automatically if you've called the model in the past 15 days
During the notice period, calls to a deprecated model will include a deprecation warning in the API response.
During the notice period, the model remains available, though in some cases we may reduce infrastructure capacity. We always provide a recommended replacement, and when needed, offer migration guidance to help the transition.
After the sunset date, requests to the model will automatically route to a model of similar processing power at the same or lower price. If routing is not possible for technical or safety reasons, the API will return a 410 Gone response. If a deprecated model was selected via a trait (such as `default_code`, `default_vision`, or `fastest`) that trait will be reassigned to a compatible replacement.
We never remove models silently or alter behavior without versioning. You’ll always know what’s running and how to prepare for what’s next.
Performance-only upgrades: We may roll out improvements that preserve model behavior while improving performance, latency, or cost efficiency. These updates are backward-compatible and require no customer action.
See the [Model Deprecation Tracker](#model-deprecation-tracker) below. For earlier announcements, consult the [changelog](https://featurebase.venice.ai/changelog) and our [Discord server](https://discord.gg/askvenice).
## How models are selected for the Venice API
We carefully select which models to make available based on performance, reliability, and real-world developer needs. To be included, a model must demonstrate strong performance, behave consistently under OpenAI-compatible endpoints, and offer a clear improvement over at least one of the models we already support.
Models we're evaluating may first be released in [beta](/overview/beta-models) to gather feedback and validate performance at scale.
We don’t expose models that are redundant, unproven, or not ready for consistent production use. Our goal is to keep the Venice API clean, capable, and optimized for what developers actually build.
Learn more in [Model Deprecations](/overview/deprecations#model-deprecations) and Current Model List.
## Versioning and Aliases
All Venice models are identified by a unique, permanent ID. For example:
`venice-uncensored`
`zai-org-glm-4.7`
`zai-org-glm-5`
`qwen3-vl-235b-a22b`
Model IDs are stable. If there's a breaking change, we will release a new model ID (for example, add a version like v2). If there are no breaking changes, we may update the existing model and will communicate significant changes.
To provide flexibility, Venice also maintains symbolic aliases, implemented through traits, that point to the recommended default model for a given task:
Traits offer a stable abstraction for selecting models while giving Venice the flexibility to improve the underlying implementation. Developers who prefer automatic access to the latest recommended models can rely on trait-based aliases.
For applications that require strict consistency and predictable behavior, we recommend referencing fixed model IDs.
## Feedback
You can submit your feedback or request through our [Featurebase portal](https://featurebase.venice.ai). We maintain a public [changelog](https://featurebase.venice.ai/changelog), roadmap tracker, and transparent rationale for adding, upgrading, or removing models, and we encourage continuous community participation.
## Model Deprecation Tracker
The following models are scheduled for deprecation or have been recently deprecated. We recommend migrating to suggested replacements before the removal date. Models remain listed for 30 days after their removal date.
### Checking Deprecation Status via the API
You can check if a model is scheduled for retirement by calling the [List Models](/api-reference/endpoint/models/list) endpoint. Models with a retirement date include a `deprecation` object in their `model_spec`:
```json theme={"system"}
{
"id": "some-model-id",
"model_spec": {
"name": "Some Model",
"privacy": "private",
"deprecation": {
"date": "2025-03-01T00:00:00.000Z"
}
},
"type": "text",
"object": "model",
"owned_by": "venice.ai"
}
```
The `deprecation` object only appears when a model is scheduled for retirement. You can check `if (model.model_spec.deprecation)` to know if a model is being retired, and use the ISO 8601 date to warn users or plan migrations.
# API Pricing
Source: https://docs.venice.ai/overview/pricing
Prices per 1M tokens unless noted. All prices in USD. 1 Diem = \$1/day of compute.
## Text Models
### Chat Completions
| Model | ID | Input Price | Output Price | Cache Read | Cache Write | Context | Privacy |
| --------------------------------- | -------------------------------------- | ----------- | ------------ | ---------- | ----------- | ------- | -------------- |
| Aion 3.0 | `aion-labs-aion-3-0` | \$3.75 | \$7.50 | \$0.94 | - | 128K | Anonymized |
| Aion 3.0 Mini | `aion-labs-aion-3-0-mini` | \$0.88 | \$1.75 | \$0.23 | - | 128K | Anonymized |
| Claude Fable 5 | `claude-fable-5` | \$12.00 | \$60.00 | \$1.20 | \$15.00 | 1000K | Anonymized |
| Claude Opus 4.5 | `claude-opus-4-5` | \$6.00 | \$30.00 | \$0.60 | \$7.50 | 198K | Anonymized |
| Claude Opus 4.6 (Beta) | `claude-opus-4-6` | \$6.00 | \$30.00 | \$0.60 | \$7.50 | 1000K | Anonymized |
| Claude Opus 4.7 | `claude-opus-4-7` | \$6.00 | \$30.00 | \$0.60 | \$7.50 | 1000K | Anonymized |
| Claude Opus 4.8 | `claude-opus-4-8` | \$6.00 | \$30.00 | \$0.60 | \$7.50 | 1000K | Anonymized |
| Claude Opus 4.8 Fast (Beta) | `claude-opus-4-8-fast` | \$12.00 | \$60.00 | \$1.20 | \$15.00 | 1000K | Anonymized |
| Claude Sonnet 4.5 | `claude-sonnet-4-5` | \$3.75 | \$18.75 | \$0.38 | \$4.69 | 198K | Anonymized |
| Claude Sonnet 4.6 (Beta) | `claude-sonnet-4-6` | \$3.60 | \$18.00 | \$0.36 | \$4.50 | 1000K | Anonymized |
| Claude Sonnet 5 (Beta) | `claude-sonnet-5` | \$3.00 | \$15.00 | \$0.30 | \$3.75 | 1000K | Anonymized |
| DeepSeek V3.2 | `deepseek-v3.2` | \$0.33 | \$0.48 | \$0.16 | - | 160K | Private |
| DeepSeek V4 Flash | `deepseek-v4-flash` | \$0.14 | \$0.28 | \$0.03 | - | 1000K | Anonymized |
| DeepSeek V4 Flash (Beta) | `e2ee-deepseek-v4-flash` | \$0.18 | \$0.37 | \$0.04 | - | 1000K | E2EE · Private |
| DeepSeek V4 Pro | `deepseek-v4-pro` | \$1.65 | \$3.30 | \$0.33 | - | 1000K | Anonymized |
| Gemini 3 Flash Preview | `gemini-3-flash-preview` | \$0.70 | \$3.75 | \$0.07 | - | 256K | Anonymized |
| Gemini 3.1 Pro Preview | `gemini-3-1-pro-preview` | \$2.50 | \$15.00 | \$0.50 | \$0.50 | 1000K | Anonymized |
| ↳ >200K Context | | \$5.00 | \$22.50 | \$0.50 | \$0.50 | | |
| Gemini 3.5 Flash | `gemini-3-5-flash` | \$1.55 | \$9.45 | \$0.15 | \$0.09 | 1000K | Anonymized |
| Gemma 3 27B (Beta) | `e2ee-gemma-3-27b-p` | \$0.14 | \$0.50 | - | - | 40K | E2EE · Private |
| Gemma 4 26B A4B Uncensored (Beta) | `e2ee-gemma-4-26b-a4b-uncensored-p` | \$0.19 | \$0.88 | - | - | 64K | E2EE · Private |
| Gemma 4 31B Instruct (Beta) | `e2ee-gemma-4-31b` | \$0.14 | \$0.43 | \$0.03 | - | 32K | E2EE · Private |
| Gemma 4 Uncensored | `gemma-4-uncensored` | \$0.16 | \$0.50 | - | - | 256K | Private |
| GLM 4.6 | `zai-org-glm-4.6` | \$0.43 | \$1.75 | \$0.08 | - | 198K | Private |
| GLM 4.7 | `zai-org-glm-4.7` | \$0.55 | \$2.65 | \$0.11 | - | 198K | Private |
| GLM 4.7 (Beta) | `e2ee-glm-4-7-p` | \$1.10 | \$4.15 | - | - | 128K | E2EE · Private |
| GLM 4.7 Flash | `zai-org-glm-4.7-flash` | \$0.13 | \$0.50 | - | - | 128K | Private |
| GLM 4.7 Flash Heretic | `olafangensan-glm-4.7-flash-heretic` | \$0.07 | \$0.40 | \$0.04 | - | 200K | Private |
| GLM 5 | `zai-org-glm-5` | \$1.00 | \$3.20 | \$0.20 | - | 198K | Private |
| GLM 5 Turbo | `z-ai-glm-5-turbo` | \$1.20 | \$4.00 | \$0.24 | - | 200K | Anonymized |
| GLM 5.1 (Beta) | `zai-org-glm-5-1` | \$1.54 | \$4.84 | \$0.29 | - | 200K | Private |
| GLM 5.1 (Beta) | `e2ee-glm-5-1` | \$1.10 | \$4.15 | - | - | 200K | E2EE · Private |
| GLM 5.2 (Beta) | `zai-org-glm-5-2` | \$1.40 | \$4.40 | \$0.26 | - | 1000K | Private |
| GLM 5.2 (Beta) | `e2ee-glm-5-2-p` | \$1.75 | \$5.75 | - | - | 524K | E2EE · Private |
| GLM 5V Turbo (Beta) | `z-ai-glm-5v-turbo` | \$1.50 | \$5.00 | \$0.30 | - | 200K | Anonymized |
| Google Gemma 3 27B Instruct | `google-gemma-3-27b-it` | \$0.12 | \$0.20 | - | - | 198K | Private |
| Google Gemma 4 26B A4B Instruct | `google-gemma-4-26b-a4b-it` | \$0.13 | \$0.40 | \$0.05 | - | 256K | Private |
| Google Gemma 4 31B Instruct | `google-gemma-4-31b-it` | \$0.12 | \$0.36 | \$0.09 | - | 256K | Private |
| GPT OSS 120B (Beta) | `e2ee-gpt-oss-120b-p` | \$0.13 | \$0.65 | - | - | 128K | E2EE · Private |
| GPT OSS 20B (Beta) | `e2ee-gpt-oss-20b-p` | \$0.05 | \$0.19 | - | - | 128K | E2EE · Private |
| GPT-4o | `openai-gpt-4o-2024-11-20` | \$3.13 | \$12.50 | - | - | 128K | Anonymized |
| GPT-4o Mini | `openai-gpt-4o-mini-2024-07-18` | \$0.19 | \$0.75 | \$0.09 | - | 128K | Anonymized |
| GPT-5.2 | `openai-gpt-52` | \$2.19 | \$17.50 | \$0.22 | - | 256K | Anonymized |
| GPT-5.2 Codex | `openai-gpt-52-codex` | \$2.19 | \$17.50 | \$0.22 | - | 256K | Anonymized |
| GPT-5.3 Codex (Beta) | `openai-gpt-53-codex` | \$2.19 | \$17.50 | \$0.22 | - | 400K | Anonymized |
| GPT-5.4 (Beta) | `openai-gpt-54` | \$3.13 | \$18.80 | \$0.31 | - | 1000K | Anonymized |
| GPT-5.4 Mini (Beta) | `openai-gpt-54-mini` | \$0.94 | \$5.63 | \$0.09 | - | 400K | Anonymized |
| GPT-5.4 Pro (Beta) | `openai-gpt-54-pro` | \$37.50 | \$225.00 | - | - | 1000K | Anonymized |
| ↳ >272K Context | | \$75.00 | \$337.50 | - | - | | |
| GPT-5.5 (Beta) | `openai-gpt-55` | \$6.25 | \$37.50 | \$0.63 | - | 1000K | Anonymized |
| ↳ >272K Context | | \$12.50 | \$56.25 | \$1.25 | - | | |
| GPT-5.5 Pro (Beta) | `openai-gpt-55-pro` | \$37.50 | \$225.00 | - | - | 1000K | Anonymized |
| GPT-5.6 Luna (Beta) | `openai-gpt-56-luna` | \$1.25 | \$7.50 | \$0.13 | \$1.56 | 1000K | Anonymized |
| GPT-5.6 Luna Pro (Beta) | `openai-gpt-56-luna-pro` | \$1.25 | \$7.50 | \$0.13 | \$1.56 | 1000K | Anonymized |
| GPT-5.6 Sol (Beta) | `openai-gpt-56-sol` | \$6.25 | \$37.50 | \$0.63 | \$7.81 | 1000K | Anonymized |
| GPT-5.6 Sol Pro (Beta) | `openai-gpt-56-sol-pro` | \$6.25 | \$37.50 | \$0.63 | \$7.81 | 1000K | Anonymized |
| GPT-5.6 Terra (Beta) | `openai-gpt-56-terra` | \$3.13 | \$18.75 | \$0.31 | \$3.91 | 1000K | Anonymized |
| GPT-5.6 Terra Pro (Beta) | `openai-gpt-56-terra-pro` | \$3.13 | \$18.75 | \$0.31 | \$3.91 | 1000K | Anonymized |
| Grok 4.20 | `grok-4-20` | \$1.42 | \$2.83 | \$0.23 | - | 2000K | Private |
| ↳ >200K Context | | \$2.83 | \$5.67 | \$0.45 | - | | |
| Grok 4.20 Multi-Agent | `grok-4-20-multi-agent` | \$1.42 | \$2.83 | \$0.23 | - | 2000K | Private |
| ↳ >200K Context | | \$2.83 | \$5.67 | \$0.45 | - | | |
| Grok 4.3 | `grok-4-3` | \$1.42 | \$2.83 | \$0.23 | - | 1000K | Private |
| ↳ >200K Context | | \$2.83 | \$5.67 | \$0.45 | - | | |
| Grok 4.5 | `grok-4-5` | \$2.27 | \$6.80 | \$0.34 | - | 500K | Private |
| ↳ >200K Context | | \$4.53 | \$13.60 | \$0.68 | - | | |
| Grok Build 0.1 (Beta) | `grok-build-0-1` | \$1.00 | \$2.00 | \$0.20 | - | 256K | Private |
| ↳ >200K Context | | \$2.00 | \$4.00 | \$0.40 | - | | |
| Hermes 3 Llama 3.1 405b | `hermes-3-llama-3.1-405b` | \$1.10 | \$3.00 | - | - | 128K | Private |
| Inkling (Beta) | `inkling` | \$1.25 | \$5.06 | \$0.21 | - | 1000K | Private |
| Kimi K2.5 | `kimi-k2-5` | \$0.56 | \$3.50 | \$0.22 | - | 256K | Private |
| Kimi K2.6 | `kimi-k2-6` | \$0.75 | \$3.50 | \$0.16 | - | 256K | Private |
| Kimi K2.7 Code (Beta) | `kimi-k2-7-code` | \$0.75 | \$3.50 | \$0.16 | - | 256K | Private |
| Kimi K3 (Beta) | `kimi-k3` | \$3.75 | \$18.75 | \$0.38 | - | 1000K | Anonymized |
| Llama 3.2 3B | `llama-3.2-3b` | \$0.15 | \$0.60 | - | - | 128K | Private |
| Llama 3.3 70B | `llama-3.3-70b` | \$0.70 | \$2.80 | - | - | 128K | Private |
| Mercury 2 (Beta) | `mercury-2` | \$0.31 | \$0.94 | \$0.03 | - | 128K | Anonymized |
| MiMo-V2.5 | `xiaomi-mimo-v2-5` | \$0.14 | \$0.28 | \$0.05 | - | 1000K | Private |
| MiniMax M2.5 | `minimax-m25` | \$0.27 | \$0.95 | \$0.03 | - | 198K | Private |
| MiniMax M2.7 | `minimax-m27` | \$0.38 | \$1.50 | \$0.07 | - | 198K | Private |
| MiniMax M3 Preview (Beta) | `minimax-m3-preview` | \$0.30 | \$1.20 | \$0.06 | - | 524K | Private |
| Mistral Small 3.2 24B Instruct | `mistral-small-3-2-24b-instruct` | \$0.09 | \$0.25 | - | - | 256K | Private |
| Mistral Small 4 (Beta) | `mistral-small-2603` | \$0.19 | \$0.75 | - | - | 256K | Private |
| Nemotron Cascade 2 30B A3B (Beta) | `nvidia-nemotron-cascade-2-30b-a3b` | \$0.14 | \$0.80 | - | - | 256K | Private |
| NVIDIA Nemotron 3 Nano 30B (Beta) | `nvidia-nemotron-3-nano-30b-a3b` | \$0.07 | \$0.30 | - | - | 128K | Private |
| NVIDIA Nemotron 3 Ultra | `nvidia-nemotron-3-ultra-550b-a55b` | \$0.63 | \$3.13 | \$0.19 | - | 256K | Private |
| OpenAI GPT OSS 120B | `openai-gpt-oss-120b` | \$0.07 | \$0.30 | - | - | 128K | Private |
| Qwen 2.5 7B (Beta) | `e2ee-qwen-2-5-7b-p` | \$0.05 | \$0.13 | - | - | 32K | E2EE · Private |
| Qwen 3 235B A22B Instruct 2507 | `qwen3-235b-a22b-instruct-2507` | \$0.15 | \$0.75 | - | - | 128K | Private |
| Qwen 3 235B A22B Thinking 2507 | `qwen3-235b-a22b-thinking-2507` | \$0.45 | \$3.50 | - | - | 128K | Private |
| Qwen 3 Coder 480B Turbo (Beta) | `qwen3-coder-480b-a35b-instruct-turbo` | \$0.35 | \$1.50 | \$0.04 | - | 256K | Private |
| Qwen 3 Next 80b | `qwen3-next-80b` | \$0.35 | \$1.90 | - | - | 256K | Private |
| Qwen 3.5 35B A3B (Beta) | `qwen3-5-35b-a3b` | \$0.31 | \$1.25 | \$0.16 | - | 256K | Private |
| Qwen 3.5 397B | `qwen3-5-397b-a17b` | \$0.75 | \$4.50 | - | - | 128K | Anonymized |
| Qwen 3.5 9B | `qwen3-5-9b` | \$0.10 | \$0.15 | - | - | 256K | Private |
| Qwen 3.6 27B | `qwen3-6-27b` | \$0.33 | \$3.25 | - | - | 256K | Private |
| Qwen 3.6 27B FP8 (Beta) | `e2ee-qwen3-6-27b` | \$0.35 | \$3.46 | \$0.17 | - | 256K | E2EE · Private |
| Qwen 3.6 35B A3B FP8 (Beta) | `e2ee-qwen3-6-35b-a3b` | \$0.18 | \$1.18 | \$0.06 | - | 32K | E2EE · Private |
| Qwen 3.6 Plus Uncensored (Beta) | `qwen-3-6-plus` | \$0.63 | \$3.75 | \$0.06 | \$0.78 | 1000K | Anonymized |
| ↳ >256K Context | | \$2.50 | \$7.50 | \$0.06 | \$0.78 | | |
| Qwen 3.7 Max (Beta) | `qwen-3-7-max` | \$2.70 | \$8.05 | \$0.27 | \$3.35 | 1000K | Anonymized |
| Qwen 3.7 Plus (Beta) | `qwen-3-7-plus` | \$0.50 | \$2.00 | \$0.05 | \$0.63 | 1000K | Anonymized |
| ↳ >256K Context | | \$1.50 | \$6.00 | \$0.15 | \$1.88 | | |
| Qwen3 30B A3B (Beta) | `e2ee-qwen3-30b-a3b-p` | \$0.19 | \$0.69 | - | - | 256K | E2EE · Private |
| Qwen3 VL 235B | `qwen3-vl-235b-a22b` | \$0.21 | \$1.90 | \$0.10 | - | 128K | Private |
| Qwen3 VL 30B A3B (Beta) | `e2ee-qwen3-vl-30b-a3b-p` | \$0.25 | \$0.90 | - | - | 128K | E2EE · Private |
| Qwen3.6 35B A3B Uncensored (Beta) | `e2ee-qwen3-6-35b-a3b-uncensored-p` | \$0.38 | \$1.88 | - | - | 128K | E2EE · Private |
| Venice Role Play Uncensored | `venice-uncensored-role-play` | \$0.50 | \$2.00 | - | - | 128K | Private |
| Venice Uncensored 1.1 (Beta) | `e2ee-venice-uncensored-24b-p` | \$0.25 | \$1.15 | - | - | 32K | E2EE · Private |
| Venice Uncensored 1.2 | `venice-uncensored-1-2` | \$0.20 | \$0.90 | - | - | 128K | Private |
*Prices per 1M tokens. [View all models →](/models/text)*
### Embeddings
| Model | ID | Input (per 1M tokens) | Output (per 1M tokens) | Privacy |
| ------------------------------ | ----------------------------------------------- | --------------------- | ---------------------- | ---------- |
| BGE-EN-ICL | `text-embedding-bge-en-icl` | \$0.01 | \$0.01 | Private |
| BGE-M3 | `text-embedding-bge-m3` | \$0.15 | \$0.60 | Private |
| Gemini Embedding 2 Preview | `gemini-embedding-2-preview` | \$0.25 | \$0.25 | Anonymized |
| Multilingual E5 Large Instruct | `text-embedding-multilingual-e5-large-instruct` | \$0.01 | \$0.01 | Private |
| Nemotron Embed VL 1B v2 | `text-embedding-nemotron-embed-vl-1b-v2` | \$0.01 | \$0.01 | Private |
| Qwen3 Embedding 0.6B | `text-embedding-qwen3-0-6b` | \$0.01 | \$0.01 | Private |
| Qwen3 Embedding 8B | `text-embedding-qwen3-8b` | \$0.01 | \$0.01 | Private |
| Text Embedding 3 Large | `text-embedding-3-large` | \$0.16 | \$0.16 | Anonymized |
| Text Embedding 3 Small | `text-embedding-3-small` | \$0.03 | \$0.03 | Anonymized |
## Media Models
### Image Generation
#### Generation
| Model | ID | Price | Privacy |
| -------------------------------- | ---------------------------- | -------------------------------- | ---------- |
| Recraft V4 Pro | `recraft-v4-pro` | Per Image: \$0.29 | Anonymized |
| GPT Image 2 | `gpt-image-2` | 1K: $0.27, 2K: $0.51, 4K: \$0.84 | Anonymized |
| GPT Image 1.5 | `gpt-image-1-5` | Per Image: \$0.26 | Anonymized |
| Nano Banana Pro | `nano-banana-pro` | 1K: $0.18, 2K: $0.23, 4K: \$0.35 | Anonymized |
| Luma Uni-1 Max | `luma-uni-1-max` | Per Image: \$0.12 | Anonymized |
| Nano Banana 2 | `nano-banana-2` | 1K: $0.10, 2K: $0.14, 4K: \$0.19 | Anonymized |
| Qwen Image 2 Pro | `qwen-image-2-pro` | Per Image: \$0.10 | Anonymized |
| Wan 2.7 Pro | `wan-2-7-pro-text-to-image` | Per Image: \$0.09 | Anonymized |
| Flux 2 Max | `flux-2-max` | Per Image: \$0.09 | Anonymized |
| Grok Imagine High Quality (SOTA) | `grok-imagine-image-quality` | 1K: $0.06, 2K: $0.09 | Private |
| Ideogram V4 | `ideogram-v4` | Per Image: \$0.06 | Anonymized |
| ImagineArt 1.5 Pro | `imagineart-1.5-pro` | Per Image: \$0.06 | Anonymized |
| Nano Banana 2 Lite | `nano-banana-2-lite` | Per Image: \$0.06 | Anonymized |
| Seedream V5 Pro | `seedream-v5-pro` | 1K: $0.06, 2K: $0.11 | Anonymized |
| Luma Uni-1 | `luma-uni-1` | Per Image: \$0.05 | Anonymized |
| Qwen Image 2 | `qwen-image-2` | Per Image: \$0.05 | Anonymized |
| Recraft V4 | `recraft-v4` | Per Image: \$0.05 | Anonymized |
| Seedream V4.5 | `seedream-v4` | Per Image: \$0.05 | Anonymized |
| Seedream V5 Lite | `seedream-v5-lite` | Per Image: \$0.05 | Anonymized |
| Krea 2 Turbo | `krea-2-turbo` | 1K: $0.04, 2K: $0.06 | Private |
| Wan 2.7 | `wan-2-7-text-to-image` | Per Image: \$0.04 | Anonymized |
| Background Remover | `bria-bg-remover` | Per Image: \$0.03 | Anonymized |
| Flux 2 Pro | `flux-2-pro` | Per Image: \$0.03 | Anonymized |
| Grok Imagine | `grok-imagine-image` | 1K: $0.03, 2K: $0.04 | Private |
| Qwen Image | `qwen-image` | Per Image: \$0.03 | Anonymized |
| Anime (WAI) | `wai-Illustrious` | Per Image: \$0.01 | Private |
| Chroma | `chroma` | Per Image: \$0.01 | Private |
| Lustify SDXL | `lustify-sdxl` | Per Image: \$0.01 | Private |
| Lustify v7 | `lustify-v7` | Per Image: \$0.01 | Private |
| Lustify v8 | `lustify-v8` | Per Image: \$0.01 | Private |
| Venice SD35 | `venice-sd35` | Per Image: \$0.01 | Private |
| Z-Image Turbo | `z-image-turbo` | Per Image: \$0.01 | Private |
| Hunyuan Image 3.0 (Beta) | `hunyuan-image-v3` | Per Image: \$0.09 | Private |
| Krea v2 Large (Beta) | `krea-v2-large` | Per Image: \$0.07 | Anonymized |
| Krea v2 Medium (Beta) | `krea-v2-medium` | Per Image: \$0.04 | Anonymized |
#### Upscaling
| Model | ID | 2x Upscale | 4x Upscale |
| -------------- | ---------- | ---------- | ---------- |
| Image Upscaler | `upscaler` | \$0.02 | \$0.08 |
#### Editing
| Model | ID | Per Edit | Extra Input Image |
| ------------------------- | --------------------------- | -------- | ----------------- |
| FireRed Edit | `firered-image-edit` | \$0.04 | - |
| Flux 2 Max | `flux-2-max-edit` | \$0.12 | \$0.03 |
| GPT Image 1.5 | `gpt-image-1-5-edit` | \$0.31 | \$0.03 |
| GPT Image 2 | `gpt-image-2-edit` | \$0.34 | \$0.0092 |
| Grok Imagine | `grok-imagine-edit` | \$0.03 | \$0.0023 |
| Grok Imagine High Quality | `grok-imagine-quality-edit` | \$0.06 | \$0.01 |
| Luma Uni-1 | `luma-uni-1-edit` | \$0.06 | - |
| Luma Uni-1 Max | `luma-uni-1-max-edit` | \$0.13 | - |
| Nano Banana 2 | `nano-banana-2-edit` | \$0.10 | - |
| Nano Banana 2 Lite | `nano-banana-2-lite-edit` | \$0.06 | - |
| Nano Banana Pro | `nano-banana-pro-edit` | \$0.18 | - |
| Qwen Edit Uncensored | `qwen-edit-uncensored` | \$0.04 | - |
| Qwen Image 2 | `qwen-image-2-edit` | \$0.05 | - |
| Qwen Image 2 Pro | `qwen-image-2-pro-edit` | \$0.10 | - |
| Seedream V4.5 | `seedream-v4-edit` | \$0.05 | - |
| Seedream V5 Lite | `seedream-v5-lite-edit` | \$0.05 | - |
| Seedream V5 Pro | `seedream-v5-pro-edit` | \$0.11 | \$0.0035 |
| Wan 2.7 Pro Edit | `wan-2-7-pro-edit` | \$0.09 | - |
| Qwen Image | `qwen-image` | \$0.04 | - |
> **Editing with multiple input images:** The **Per Edit** price includes the first input image. Models that list an **Extra Input Image** price charge that fee for each additional input image beyond the first. Example: editing with 3 input images on a model priced at $0.11 per edit with a $0.0035 extra-image fee costs $0.11 + 2 × $0.0035 = \$0.117.
### Audio
#### Text-to-Speech
| Model | ID | Per 1M Characters | Privacy |
| --------------------------- | --------------------------- | ----------------- | ---------- |
| Chatterbox HD (Resemble AI) | `tts-chatterbox-hd` | \$50.00 | Private |
| ElevenLabs Turbo v2.5 | `tts-elevenlabs-turbo-v2-5` | \$62.50 | Anonymized |
| Gemini 3.1 Flash TTS | `tts-gemini-3-1-flash` | \$187.50 | Anonymized |
| Gradium TTS | `tts-gradium-v1` | \$47.50 | Anonymized |
| Inworld TTS-1.5 Max | `tts-inworld-1-5-max` | \$12.50 | Anonymized |
| Kokoro Text to Speech | `tts-kokoro` | \$3.50 | Private |
| MiniMax Speech-02 HD | `tts-minimax-speech-02-hd` | \$125.00 | Anonymized |
| Orpheus TTS | `tts-orpheus` | \$62.50 | Private |
| Qwen 3 TTS 0.6B | `tts-qwen3-0-6b` | \$87.50 | Private |
| Qwen 3 TTS 1.7B | `tts-qwen3-1-7b` | \$112.50 | Private |
| xAI TTS v1 | `tts-xai-v1` | \$18.75 | Anonymized |
#### Speech-to-Text
| Model | ID | Per Audio Second | Privacy |
| --------------------- | ----------------------------- | ---------------- | ---------- |
| ElevenLabs Scribe V2 | `elevenlabs/scribe-v2` | \$0.0002 | Anonymized |
| Parakeet ASR | `nvidia/parakeet-tdt-0.6b-v3` | \$0.0001 | Private |
| Whisper Large V3 | `openai/whisper-large-v3` | \$0.0001 | Private |
| Wizper (Whisper v3) | `fal-ai/wizper` | \$0.0001 | Private |
| xAI Speech to Text v1 | `stt-xai-v1` | \$0.0000 | Anonymized |
### Music
#### Song Generation (Duration-Based)
| Model | ID | Duration Pricing | Privacy |
| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| ACE-Step 1.5 | `ace-step-15` | 60s: $0.03, 90s: $0.04, 120s: $0.05, 150s: $0.06, 180s: $0.07, 210s: $0.08 | Anonymized |
| ElevenLabs Music | `elevenlabs-music` | 60s: $0.69, 120s: $1.38, 180s: $2.08, 240s: $2.76, 300s: $3.45, 360s: $4.15, 420s: $4.84, 480s: $5.52, 540s: $6.22, 600s: $6.90 | Anonymized |
#### Song Generation (Per-Generation)
| Model | ID | Per Generation | Privacy |
| ----------------- | ------------------- | -------------- | ---------- |
| Lyria 3 Pro | `lyria-3-pro` | \$0.10 | Anonymized |
| MiniMax Music 2.0 | `minimax-music-v2` | \$0.04 | Anonymized |
| MiniMax Music 2.5 | `minimax-music-v25` | \$0.18 | Anonymized |
| MiniMax Music 2.6 | `minimax-music-v26` | \$0.18 | Anonymized |
| Stable Audio 2.5 | `stable-audio-25` | \$0.19 | Anonymized |
#### Sound Effects (Per-Second)
| Model | ID | Per Second | Privacy |
| ------------------------ | ----------------------------- | ---------- | ---------- |
| ElevenLabs Sound Effects | `elevenlabs-sound-effects-v2` | \$0.0023 | Anonymized |
| MMAudio V2 | `mmaudio-v2-text-to-audio` | \$0.0009 | Anonymized |
| Seed Audio 1.0 | `seed-audio-1-0` | \$0.0029 | Anonymized |
For exact pricing before generation, use the [Audio Quote API](/api-reference/endpoint/audio/quote). Duration-based models have fixed price tiers, while per-second models charge based on output length.
### Video
Video pricing varies by resolution and duration. Visit the [Video Models page](/models/video) for exact quotes, or use the [Video Quote API](/api-reference/endpoint/video/quote).
| Model | ID | Type | Pricing | Privacy |
| -------------------------------- | ----------------------------------------- | -------------- | -------- | ---------- |
| Gemini Omni Flash | `gemini-omni-flash-text-to-video` | Text to Video | Variable | Anonymized |
| Gemini Omni Flash | `gemini-omni-flash-image-to-video` | Image to Video | Variable | Anonymized |
| Gemini Omni Flash R2V | `gemini-omni-flash-reference-to-video` | Text to Video | Variable | Anonymized |
| Grok Imagine 1.5 Private | `grok-imagine-1-5-image-to-video-private` | Image to Video | Variable | Private |
| Grok Imagine Private | `grok-imagine-text-to-video-private` | Text to Video | Variable | Private |
| Grok Imagine Private | `grok-imagine-image-to-video-private` | Image to Video | Variable | Private |
| Grok Imagine Private | `grok-imagine-video-to-video-private` | Text to Video | Variable | Private |
| Grok Imagine R2V Private | `grok-imagine-reference-to-video-private` | Text to Video | Variable | Private |
| HappyHorse 1.0 | `happyhorse-1-0-text-to-video` | Text to Video | Variable | Anonymized |
| HappyHorse 1.0 | `happyhorse-1-0-image-to-video` | Image to Video | Variable | Anonymized |
| HappyHorse 1.0 Edit | `happyhorse-1-0-video-to-video` | Text to Video | Variable | Anonymized |
| HappyHorse 1.0 Reference | `happyhorse-1-0-reference-to-video` | Text to Video | Variable | Anonymized |
| HappyHorse 1.1 | `happyhorse-1-1-text-to-video` | Text to Video | Variable | Anonymized |
| HappyHorse 1.1 | `happyhorse-1-1-image-to-video` | Image to Video | Variable | Anonymized |
| HappyHorse 1.1 Reference | `happyhorse-1-1-reference-to-video` | Text to Video | Variable | Anonymized |
| Kling 2.5 Turbo Pro | `kling-2.5-turbo-pro-text-to-video` | Text to Video | Variable | Anonymized |
| Kling 2.5 Turbo Pro | `kling-2.5-turbo-pro-image-to-video` | Image to Video | Variable | Anonymized |
| Kling 2.6 Pro | `kling-2.6-pro-text-to-video` | Text to Video | Variable | Anonymized |
| Kling 2.6 Pro | `kling-2.6-pro-image-to-video` | Image to Video | Variable | Anonymized |
| Kling O3 4K | `kling-o3-4k-text-to-video` | Text to Video | Variable | Anonymized |
| Kling O3 4K | `kling-o3-4k-image-to-video` | Image to Video | Variable | Anonymized |
| Kling O3 4K R2V | `kling-o3-4k-reference-to-video` | Text to Video | Variable | Anonymized |
| Kling O3 Pro | `kling-o3-pro-text-to-video` | Text to Video | Variable | Anonymized |
| Kling O3 Pro | `kling-o3-pro-image-to-video` | Image to Video | Variable | Anonymized |
| Kling O3 Pro R2V (Beta) | `kling-o3-pro-reference-to-video` | Text to Video | Variable | Anonymized |
| Kling O3 Standard | `kling-o3-standard-text-to-video` | Text to Video | Variable | Anonymized |
| Kling O3 Standard | `kling-o3-standard-image-to-video` | Image to Video | Variable | Anonymized |
| Kling O3 Standard R2V (Beta) | `kling-o3-standard-reference-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 4K | `kling-v3-4k-text-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 4K R2V | `kling-v3-4k-reference-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 Pro | `kling-v3-pro-text-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 Pro | `kling-v3-pro-image-to-video` | Image to Video | Variable | Anonymized |
| Kling V3 Pro Motion Control | `kling-v3-pro-motion-control` | Text to Video | Variable | Anonymized |
| Kling V3 Standard | `kling-v3-standard-text-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 Standard | `kling-v3-standard-image-to-video` | Image to Video | Variable | Anonymized |
| Kling V3 Standard Motion Control | `kling-v3-standard-motion-control` | Text to Video | Variable | Anonymized |
| Kling V3 Turbo Pro | `kling-v3-turbo-pro-text-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 Turbo Pro | `kling-v3-turbo-pro-image-to-video` | Image to Video | Variable | Anonymized |
| Kling V3 Turbo Standard | `kling-v3-turbo-standard-text-to-video` | Text to Video | Variable | Anonymized |
| Kling V3 Turbo Standard | `kling-v3-turbo-standard-image-to-video` | Image to Video | Variable | Anonymized |
| Longcat Distilled | `longcat-distilled-image-to-video` | Image to Video | Variable | Private |
| Longcat Distilled | `longcat-distilled-text-to-video` | Text to Video | Variable | Private |
| Longcat Full Quality | `longcat-image-to-video` | Image to Video | Variable | Private |
| Longcat Full Quality | `longcat-text-to-video` | Text to Video | Variable | Private |
| LTX Video 2.3 Fast | `ltx-2-v2-3-fast-image-to-video` | Image to Video | Variable | Anonymized |
| LTX Video 2.3 Fast | `ltx-2-v2-3-fast-text-to-video` | Text to Video | Variable | Anonymized |
| LTX Video 2.3 Full Quality | `ltx-2-v2-3-full-image-to-video` | Image to Video | Variable | Anonymized |
| LTX Video 2.3 Full Quality | `ltx-2-v2-3-full-text-to-video` | Text to Video | Variable | Anonymized |
| Ovi | `ovi-image-to-video` | Image to Video | Variable | Private |
| PixVerse C1 | `pixverse-c1-text-to-video` | Text to Video | Variable | Anonymized |
| PixVerse C1 | `pixverse-c1-image-to-video` | Image to Video | Variable | Anonymized |
| PixVerse C1 R2V | `pixverse-c1-reference-to-video` | Text to Video | Variable | Anonymized |
| PixVerse C1 Transition | `pixverse-c1-transition` | Text to Video | Variable | Anonymized |
| PixVerse v5.6 | `pixverse-v5.6-text-to-video` | Text to Video | Variable | Anonymized |
| PixVerse v5.6 | `pixverse-v5.6-image-to-video` | Image to Video | Variable | Anonymized |
| PixVerse v5.6 Transition | `pixverse-v5.6-transition` | Text to Video | Variable | Anonymized |
| Runway Gen-4 Aleph | `runway-gen4-aleph` | Text to Video | Variable | Anonymized |
| Runway Gen-4 Turbo | `runway-gen4-turbo` | Text to Video | Variable | Anonymized |
| Runway Gen-4.5 | `runway-gen4-5` | Text to Video | Variable | Anonymized |
| Runway Gen-4.5 | `runway-gen4-5-text` | Text to Video | Variable | Anonymized |
| Topaz Video Upscale | `topaz-video-upscale` | Text to Video | Variable | Anonymized |
| Veo 3 Fast | `veo3-fast-text-to-video` | Text to Video | Variable | Anonymized |
| Veo 3 Fast | `veo3-fast-image-to-video` | Image to Video | Variable | Anonymized |
| Veo 3 Full Quality | `veo3-full-text-to-video` | Text to Video | Variable | Anonymized |
| Veo 3 Full Quality | `veo3-full-image-to-video` | Image to Video | Variable | Anonymized |
| Veo 3.1 Fast | `veo3.1-fast-text-to-video` | Text to Video | Variable | Anonymized |
| Veo 3.1 Fast | `veo3.1-fast-image-to-video` | Image to Video | Variable | Anonymized |
| Veo 3.1 Full Quality | `veo3.1-full-text-to-video` | Text to Video | Variable | Anonymized |
| Veo 3.1 Full Quality | `veo3.1-full-image-to-video` | Image to Video | Variable | Anonymized |
| Vidu Q3 | `vidu-q3-text-to-video` | Text to Video | Variable | Anonymized |
| Vidu Q3 | `vidu-q3-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.1 Pro | `wan-2.1-pro-image-to-video` | Image to Video | Variable | Private |
| Wan 2.2 A14B | `wan-2.2-a14b-text-to-video` | Text to Video | Variable | Private |
| Wan 2.5 Preview | `wan-2.5-preview-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.5 Preview | `wan-2.5-preview-text-to-video` | Text to Video | Variable | Anonymized |
| Wan 2.6 | `wan-2.6-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.6 | `wan-2.6-text-to-video` | Text to Video | Variable | Anonymized |
| Wan 2.6 Flash | `wan-2.6-flash-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.7 | `wan-2-7-text-to-video` | Text to Video | Variable | Anonymized |
| Wan 2.7 | `wan-2-7-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.7 Edit | `wan-2-7-video-to-video` | Text to Video | Variable | Anonymized |
| Wan 2.7 Enhanced | `wan-2-7-enhanced-image-to-video` | Image to Video | Variable | Anonymized |
| Wan 2.7 Enhanced (Beta) | `wan-2-7-enhanced-text-to-video` | Text to Video | Variable | Anonymized |
| Wan 2.7 Reference | `wan-2-7-reference-to-video` | Text to Video | Variable | Anonymized |
## Additional Features
### Web Search and Scraping
| Feature | Config | Pricing |
| -------------- | --------------------------- | ----------------------- |
| Web Search | `enable_web_search: true` | \$10.00 per 1K requests |
| Web Scraping | `enable_web_scraping: true` | \$10.00 per 1K URLs |
| X Search (xAI) | `enable_x_search: true` | \$10.00 per 1K results |
**Web Scraping** automatically detects up to 5 URLs per request, scrapes and converts content into structured markdown, and adds the extracted text into model context. Only successfully scraped URLs are billed.
**X Search** enables xAI's native search for supported Grok models (e.g., `grok-4-20-beta`). This searches both the web and X/Twitter for real-time information. Billed per search result returned by the model (e.g., if the model returns 10 search results, you are charged for 10 results at $0.01 each = $0.10).
These charges apply in addition to standard model token pricing.
## Payment Options
Buy API credits with credit card. Credits never expire.
Buy API credits with cryptocurrency. Same rates as USD.
Each Diem = \$1/day of credits that refresh daily.
### Pro Users
Pro subscribers receive a one-time \$10 API credit when upgrading to Pro. Use it to test and build small apps.
# Privacy
Source: https://docs.venice.ai/overview/privacy
How Venice handles prompts, responses, and metadata, plus the privacy modes available: anonymized, private, Trusted Execution, and end-to-end encrypted.
One of Venice's guiding principles is user privacy. The platform's architecture flows from this philosophical principle, and every component is designed with this objective in mind.
> The only way to achieve reasonable user privacy is to avoid collecting this information in the first place. This is harder to do from an engineering perspective, but we believe it is the correct approach.
The Venice API replicates the same backend privacy architecture as the Venice platform: requests pass through the Venice proxy over encrypted connections, Venice does not store or log prompt and response content for normal inference, and each selected model adds one of four privacy modes at the runtime layer: Anonymous, Private, TEE, or E2EE.
## Privacy architecture
The Venice proxy is the shared foundation for every privacy mode. Requests pass through Venice over HTTPS/TLS and are relayed without Venice storing prompt or response content. The privacy mode on the selected model determines what happens next at the provider or model runtime layer.
Venice presents model privacy in four modes. They build on the same proxy foundation and add progressively stronger protections, from obscuring identity from the provider to encrypting prompts end-to-end into a verified enclave.
Increasing privacy protection
Anonymous
Identity obscured from provider
Venice proxies the request without sending your Venice identity to the model provider. Prompt content is still visible to that provider.
Private
Zero data retention, contract-enforced
Prompt and response content is processed for inference only and is not retained after the request completes.
TEE
Hardware-isolated inference
Supported models run inside a Trusted Execution Environment with remote attestation support.
E2EE
End-to-end encrypted to a verified TEE
Your client encrypts the prompt before sending it. Venice relays ciphertext, and only the verified TEE decrypts it.
The `/models` endpoint tells you each model's privacy level. Models marked as `anonymized` are Anonymous models, and models marked as `private` are Private models. TEE and E2EE are shown separately in the model's capabilities, such as `supportsTeeAttestation` and `supportsE2EE`.
For implementation details, see the [TEE & E2EE models guide](/guides/features/tee-e2ee-models).
## TEE and E2EE
TEE and E2EE models add cryptographic and hardware-backed controls on top of Venice's default no-content-retention approach.
You want the model to run inside an attested hardware enclave, but your client can send plaintext prompts over the normal API request.
You want prompts encrypted before they leave your client and decrypted only inside a verified TEE.
The E2EE flow uses `/chat/completions` with E2EE-capable models. Your client must fetch attestation, verify the nonce and enclave evidence, encrypt `user` and `system` messages, send the `X-Venice-TEE-*` headers, stream the response, and verify/decrypt response content.
E2EE also disables features that need plaintext outside the enclave, such as web search, memory, summaries, some tool flows, and other server-side processing.
## Choosing a model
Use `/models` to see what privacy protections each model supports before you send a request.
Each model has two relevant fields:
* `model_spec.privacy` tells you the model's baseline privacy mode:
* `anonymized`: Venice hides your identity from the provider, but the provider may still see the prompt.
* `private`: Venice routes the request through zero-data-retention infrastructure.
* `model_spec.capabilities` tells you whether the model supports stronger protections:
* `supportsTeeAttestation`: the model can run inside a verifiable Trusted Execution Environment.
* `supportsE2EE`: the model can accept client-encrypted prompts that are decrypted only inside the TEE.
E2EE is a client-driven flow. Your application must encrypt the request, verify attestation, and verify/decrypt the response. See the [TEE & E2EE models guide](/guides/features/tee-e2ee-models).
```bash cURL theme={"system"}
curl https://api.venice.ai/api/v1/models \
-H "Authorization: Bearer $API_KEY_VENICE" | \
jq '.data[] | {
id,
privacy: .model_spec.privacy,
tee: .model_spec.capabilities.supportsTeeAttestation,
e2ee: .model_spec.capabilities.supportsE2EE
}'
```
A simple rule of thumb: choose `private` for zero data retention, choose `tee: true` for hardware-backed isolation, and choose `e2ee: true` when you need prompts encrypted before they leave your client.
## Operational metadata
Venice may process metadata needed for authentication, billing, abuse prevention, reliability, analytics, and support. Depending on how you use the product, this can include account or wallet identifiers, API key identifiers, request timestamps, selected model, token counts, billing amounts, rate-limit state, request IDs, IP address, browser or device information, and product event logs.
This metadata is used to operate the API and is separate from prompt and response content. Billing and usage records track details such as model, endpoint, token counts, timestamps, and account identifiers; they do not require storing the prompt or completion.
# VVV & DIEM
Source: https://docs.venice.ai/overview/vvv-diem
Fund Venice API inference with staked DIEM tokens — $1 per day of compute per token — minted from staked VVV or acquired on Base with no per-request billing.
VVV and DIEM are Venice's on-chain funding layer for the API. Stake DIEM to receive a fixed daily inference allowance — **1 DIEM = \$1 per day** of Venice API credit — without buying prepaid USD credits for every request.
| Token | Role |
| -------- | -------------------------------------------------------------------------------------------------------------- |
| **VVV** | Venice's foundational token on Base. Stake it to earn yield and to mint DIEM. |
| **sVVV** | Receipt for staked VVV. Lock sVVV at the current [Mint Rate](https://diem-calculator.venice.ai/) to mint DIEM. |
| **DIEM** | Tokenized compute unit. Stake DIEM to unlock a perpetual daily API credit equal to \$1 per DIEM. |
DIEM is an ERC-20 on Base. You can transfer, trade, or stake it. Unlike pay-as-you-go USD credits, staked DIEM is capacity you own: the daily allowance refreshes each epoch and does not dilute as network usage grows.
For model rates billed against that allowance, see [API Pricing](/overview/pricing). For wallet pay-per-request without an API key, see [x402](/guides/integrations/x402-venice-api).
## How funding works
API keys spend from the linked Venice account in this order: **DIEM**, then bundled credits, then USD.
When DIEM is the active currency:
* Your daily allocation equals the amount of DIEM you have staked (1 staked DIEM → \$1 of API credit that epoch).
* Unused DIEM in an epoch does not roll over. The allowance refreshes at **00:00 UTC**.
* Accounts need at least **0.1** staked DIEM before any DIEM balance is spendable.
* Request costs use the same USD price sheet as pay-as-you-go; DIEM is the settlement currency at a 1:1 dollar rate for that day's allocation.
Check your current and next-epoch DIEM balance in the dashboard at [venice.ai/settings/api](https://venice.ai/settings/api), or via the [Billing Balance](/api-reference/endpoint/billing/balance) endpoint.
## Get DIEM and stake it
Use the token dashboard at [venice.ai/token](https://venice.ai/token). Connect a Base wallet — do not send tokens directly to a contract address.
Buy VVV or DIEM on a Base DEX such as [Aerodrome](https://aerodrome.finance) or [Uniswap](https://app.uniswap.org), or receive DIEM transferred from another wallet.
| Asset | Contract (Base) |
| ----- | --------------------------------------------------------------------------------------------------------------------- |
| VVV | [`0xacfE6019Ed1A7Dc6f7B508C02d1b04ec88cC21bf`](https://basescan.org/token/0xacfE6019Ed1A7Dc6f7B508C02d1b04ec88cC21bf) |
| DIEM | [`0xF4d97F2da56e8c3098f3a8D538DB630A2606a024`](https://basescan.org/token/0xF4d97F2da56e8c3098f3a8D538DB630A2606a024) |
On [venice.ai/token](https://venice.ai/token), stake VVV to receive sVVV. Staked VVV earns emissions. The staking contract is [`0x321b7ff75154472B18EDb199033fF4D116F340Ff`](https://basescan.org/address/0x321b7ff75154472B18EDb199033fF4D116F340Ff#code).
Lock sVVV at the current Mint Rate to mint DIEM. The Mint Rate is how much sVVV is required per DIEM and rises as DIEM supply grows — preview it on the [DIEM calculator](https://diem-calculator.venice.ai/).
Minted DIEM appears in your wallet as an ERC-20. The locked sVVV stays locked until you burn the same amount of DIEM to unlock it.
Stake DIEM on the same token dashboard. Each staked DIEM grants **\$1 per day** of Venice API credit for as long as it remains staked.
Unstaking DIEM has a **1-day** cooldown. Unlocking the sVVV that backed minted DIEM requires burning that DIEM; unstaking the freed sVVV then has a **7-day** cooldown.
Sign in to Venice with the same wallet (Sign-In-With-Ethereum) so the staked DIEM attaches to your account, then create a key from [API settings](https://venice.ai/settings/api) or the [API key guide](/guides/getting-started/generating-api-key).
Autonomous agents can mint a key from a wallet that holds staked VVV — see [Autonomous Agent API Key Creation](/guides/getting-started/generating-api-key-agent).
Never send VVV or DIEM directly to a contract address. Tokens sent that way cannot be recovered. Always stake through [venice.ai/token](https://venice.ai/token) or a known staking transaction flow.
## Use DIEM with the API
Once DIEM is staked and linked to your account, call the API with a normal Bearer key. No extra headers or `venice_parameters` are required — Venice deducts from DIEM automatically when `consumptionCurrency` is `DIEM`.
```bash theme={"system"}
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "venice-uncensored",
"messages": [{"role": "user", "content": "Hello"}]
}'
```
### Check DIEM balance
```bash theme={"system"}
curl https://api.venice.ai/api/v1/billing/balance \
-H "Authorization: Bearer $VENICE_API_KEY"
```
Example response shape:
```json theme={"system"}
{
"canConsume": true,
"consumptionCurrency": "DIEM",
"balances": {
"diem": 90.5,
"usd": 25
},
"diemEpochAllocation": 100
}
```
| Field | Meaning |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| `balances.diem` | Remaining DIEM credit for the current epoch (`null` if you are not staking DIEM) |
| `diemEpochAllocation` | Total DIEM allocated for this epoch (staked amount). Compare with `balances.diem` for usage so far |
| `consumptionCurrency` | Currency that will be charged next (`DIEM`, `USD`, `BUNDLED_CREDITS`, or legacy `VCU`) |
| `canConsume` | Whether the account can pay for a request right now |
List models or usage analytics to see per-request costs in both USD and DIEM — they match at the \$1-per-DIEM rate for the day's allowance. See [Billing Usage](/api-reference/endpoint/billing/usage) and [Usage Analytics](/api-reference/endpoint/billing/usage-analytics).
### Cap spend per key
When creating a key, you can set an epoch consumption limit denominated in `diem` or `usd` so a single integration cannot exhaust the full daily allocation. Dashboard keys expose this as **Epoch Consumption Limits**; Web3-minted keys accept `consumptionLimit` on [`POST /api_keys/generate_web3_key`](/api-reference/endpoint/api_keys/generate_web3_key/post).
## DIEM vs USD credits vs x402
| Path | Best for | How it refreshes |
| ------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **Staked DIEM** | Predictable daily capacity, agents, fixed AI cost | Full allocation each epoch (00:00 UTC) while DIEM stays staked |
| **USD / crypto credits** | Bursts above your DIEM allocation, simple prepaid spend | Balance decreases with usage; credits never expire |
| **x402 (USDC)** | Headless pay-per-request with no API key | Wallet tops up USDC via `POST /x402/top-up`; DIEM on a linked account is still spent first when present |
If a request would exceed remaining DIEM and no USD or bundled credits cover it, the API returns `402` with `INSUFFICIENT_BALANCE`. Stake more DIEM, wait for the next epoch, or add USD credits in [Settings → API](https://venice.ai/settings/api).
## Related resources
Per-model rates charged against DIEM or USD.
Stake VVV, mint DIEM, and stake DIEM for API credit.
Mint an API key from a wallet with staked VVV on Base.
Pay per request with USDC when you do not want a long-lived API key.
Read DIEM and USD balances from the API.
Mint rate, cooldowns, yield, and other token FAQs.
# Delete API Key
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/delete
DELETE /api_keys
Delete an API key.
# Generate API Key with Web3 Wallet
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/generate_web3_key/get
GET /api_keys/generate_web3_key
Returns the token required to generate an API key via a wallet.
## Autonomous Agent API Key Creation
Please see [this guide](/guides/getting-started/generating-api-key-agent) on how to use this endpoint.
***
# Generate API Key with Web3 Wallet
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/generate_web3_key/post
POST /api_keys/generate_web3_key
Authenticates a wallet holding sVVV and creates an API key.
## Autonomous Agent API Key Creation
Please see [this guide](/guides/getting-started/generating-api-key-agent) on how to use this endpoint.
***
# Rate Limit Logs
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/rate_limit_logs
GET /api_keys/rate_limits/log
Returns the last 50 rate limits that the account exceeded.
## Experimental Endpoint
This is an experimental endpoint and may be subject to change.
## Postman Collection
For additional examples, please see this [Postman Collection](https://www.postman.com/veniceai/workspace/venice-ai-workspace/folder/38652128-b1bd9f3e-507b-46c5-ad35-be7419ea5ad3?action=share\&creator=38652128\&ctx=documentation\&active-environment=38652128-ef110f4e-d3e1-43b5-8029-4d6877e62041).
# Rate Limits and Balances
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/rate_limits
GET /api_keys/rate_limits
Return details about user balances and rate limits.
# Update API Key
Source: https://docs.venice.ai/api-reference/endpoint/api_keys/update
PATCH /api_keys
Update an existing API key. The description, expiration date, and consumption limits can be updated.
# Billing Balance
Source: https://docs.venice.ai/api-reference/endpoint/billing/balance
GET /billing/balance
Get current balance information for the authenticated user. Returns remaining DIEM/USD balances and total DIEM epoch allocation for calculating usage percentage.
# Billing Usage API (Beta)
Source: https://docs.venice.ai/api-reference/endpoint/billing/usage
GET /billing/usage
Get paginated billing usage data for the authenticated user. DEPRECATED: This endpoint is rate limited to 10 requests per minute per user and will be removed in a future release. Use GET /api/v1/billing/usage-history instead, which provides the same data with keyset pagination. Accounts created on or after 2026-07-07 are blocked with a 410 and must use the replacement endpoint.
Exports usage data for a user. Descriptions of response fields can be found below:
* **timestamp**: The timestamp the billing usage entry was created
* **sku**: The product associated with the billing usage entry
* **pricePerUnitUsd**: The price per unit in USD
* **unit**: The number of units consumed
* **amount**: The total amount charged for the billing usage entry
* **currency**: The currency charged for the billing usage entry
* **notes**: Notes about the billing usage entry
* **inferenceDetails.requestId**: The request ID associated with the inference
* **inferenceDetails.inferenceExecutionTime**: Time taken for inference execution in milliseconds
* **inferenceDetails.promptTokens**: Number of tokens requested in the prompt. Only present for LLM usage.
* **inferenceDetails.completionTokens**: Number of tokens used in the completion. Only present for LLM usage.
# Billing Usage Analytics (Beta)
Source: https://docs.venice.ai/api-reference/endpoint/billing/usage-analytics
GET /billing/usage-analytics
**Beta**: This endpoint is currently in beta and may be unstable. Request/response schemas and behavior may change without notice.
Get aggregated usage analytics for the authenticated user with breakdowns by date, model, and API key. This endpoint provides summary views of your API usage, ideal for dashboards and usage monitoring. Data is cached for 10 minutes.
This is a beta endpoint and may be unstable or change without notice.
Get aggregated usage analytics for the authenticated user, with breakdowns by date, model, and API key. This endpoint provides summary views of your API usage data for building dashboards and monitoring consumption. Data is cached for 10 minutes.
## Query Parameters
You can specify the time period for analytics using either:
* **lookback**: A relative period like "7d" (7 days), "30d" (30 days), up to "90d" (90 days)
* **startDate** and **endDate**: A custom date range in `YYYY-MM-DD` format. Both are required if either is provided.
If no parameters are specified, the default lookback period is 7 days.
## Response Fields
### lookback
The lookback period used for the query. Either in "Nd" format (e.g., "7d") or "startDate:endDate" format.
### byDate
Daily usage totals for the requested period.
* **date**: The date in `YYYY-MM-DD` format
* **USD**: Total usage in USD for that day
* **DIEM**: Total usage in DIEM for that day
### byModel
Usage breakdown by model, sorted by total spend (highest first).
* **modelName**: Display name of the model (e.g., "GLM 5")
* **unitType**: Type of units consumed (tokens, images, chars, minutes, seconds)
* **modelType**: Type of model (LLM, IMAGE, TTS, ASR, VIDEO), or null
* **totalUsd**: Total USD spent on this model
* **totalDiem**: Total DIEM spent on this model
* **totalUnits**: Total units consumed for this model
* **breakdown**: Array of usage breakdowns by type (only present if multiple types). Each entry contains:
* **type**: Token type (e.g., "Input", "Output", "Cache Read", "Cache Write")
* **usd**: USD amount for this breakdown
* **diem**: DIEM amount for this breakdown
* **units**: Number of units for this breakdown
### byModelDaily
Daily chart data for top 8 models. Each entry contains a "date" (timestamp) plus model names as keys with DIEM usage values.
### topModels
Array of the top 8 model names by usage, for chart legends.
### byKey
Usage breakdown by API key, sorted by total spend (highest first).
* **apiKeyId**: The API key ID, or null if usage was from the web app
* **description**: API key description or "Web App"
* **totalUsd**: Total USD spent via this key
* **totalDiem**: Total DIEM spent via this key
* **totalUnits**: Total units consumed via this key
### byKeyDaily
Daily chart data for top 8 API keys. Each entry contains a "date" (timestamp) plus key descriptions as keys with DIEM usage values.
### topKeyNames
Array of the top 8 API key descriptions by usage, for chart legends.
## Example Usage
```bash theme={"system"}
# Get usage analytics for the past 7 days (default)
curl -X GET "https://api.venice.ai/api/v1/billing/usage-analytics" \
-H "Authorization: Bearer YOUR_API_KEY"
# Get usage analytics for the past 30 days
curl -X GET "https://api.venice.ai/api/v1/billing/usage-analytics?lookback=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
# Get usage analytics for a specific date range
curl -X GET "https://api.venice.ai/api/v1/billing/usage-analytics?startDate=2024-01-01&endDate=2024-01-31" \
-H "Authorization: Bearer YOUR_API_KEY"
```
# Get Character
Source: https://docs.venice.ai/api-reference/endpoint/characters/get
GET /characters/{slug}
This is a preview API and may change. Returns a single character by its slug.
## Experimental Endpoint
This is an experimental endpoint and may be subject to change.
## Postman Collection
For additional examples, please see this [Postman Collection](https://www.postman.com/veniceai/workspace/062d2eda-cd10-4f2f-83b4-083178d85fc5/request/38652128-8cca56f0-e7b7-4afa-855a-c41f9a6d53e2?action=share\&source=copy-link\&creator=48156591\&ctx=documentation).
# List Characters
Source: https://docs.venice.ai/api-reference/endpoint/characters/list
GET /characters
This is a preview API and may change. Returns a list of characters supported in the API, with filtering by search, tags, categories, model, and sort options.
## Experimental Endpoint
This is an experimental endpoint and may be subject to change.
## Postman Collection
For additional examples, please see this [Postman Collection](https://www.postman.com/veniceai/workspace/venice-ai-workspace/folder/38652128-b1bd9f3e-507b-46c5-ad35-be7419ea5ad3?action=share\&creator=38652128\&ctx=documentation\&active-environment=38652128-ef110f4e-d3e1-43b5-8029-4d6877e62041).
# List Character Reviews
Source: https://docs.venice.ai/api-reference/endpoint/characters/reviews
GET /characters/{slug}/reviews
This is a preview API and may change. Returns paginated public reviews for a single character.
## Experimental Endpoint
This is an experimental endpoint and may be subject to change.
## What this returns
This endpoint returns paginated public reviews for a single character.
* Use the `slug` path parameter to identify the character.
* Use `page` and `pageSize` query parameters to paginate through reviews.
* Pagination metadata is returned both in the response body and in the `x-pagination-*` response headers.
# X402 Balance
Source: https://docs.venice.ai/api-reference/endpoint/x402/balance
GET /x402/balance/{walletAddress}
Get the x402 credit balance for a wallet address. Requires Sign-in-with-x authentication for the same EVM or Solana wallet.
# X402 Top Up
Source: https://docs.venice.ai/api-reference/endpoint/x402/top-up
POST /x402/top-up
Top up your Venice credit balance using a `PAYMENT-SIGNATURE` header (the legacy `X-402-Payment` and `X-PAYMENT` header names are also accepted). If the header is missing, the endpoint returns payment requirements.
This is the primary x402 payment endpoint. It currently returns Base and Solana USDC payment options in the `accepts` array. All inference endpoints (chat, image, audio, video) consume from the credit balance you establish here.
# X402 Transactions
Source: https://docs.venice.ai/api-reference/endpoint/x402/transactions
GET /x402/transactions/{walletAddress}
Get paginated x402 transaction history for a wallet address. Requires Sign-in-with-x authentication for the same EVM or Solana wallet.