Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions libs/openant-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,48 @@ For a repository with 1,000 units:

---

## Using non-Claude models via OpenRouter

OpenRouter exposes an Anthropic-compatible endpoint, so OpenAnt can drive
Qwen, Kimi, MiniMax, DeepSeek and similar models without a separate SDK.
Three env vars + a `--model` argument are all that's needed:

```bash
export OPENANT_LLM_BASE_URL=https://openrouter.ai/api/v1
export OPENANT_LLM_API_KEY=sk-or-v1-...
openant scan /path/to/repo --output /tmp/results --model qwen/qwen-3-coder-480b
```

When `OPENANT_LLM_BASE_URL` is unset, every Anthropic client construction
falls back to the SDK defaults, so existing Claude setups behave exactly
as before.

`--model` accepts:

| Form | Example | Effect |
|------|---------|--------|
| Alias | `opus`, `sonnet` | Resolves to the canonical Claude ID. |
| Explicit Claude ID | `claude-opus-4-6` | Used verbatim. |
| Slash-form ID | `qwen/qwen-3-coder-480b` | Used verbatim against the configured endpoint. |
| OpenCode-style prefix | `openrouter/moonshotai/kimi-k2` | Leading `openrouter/` is stripped (becomes `moonshotai/kimi-k2`). |

### Cost tracking for non-Claude models

The hardcoded pricing table only covers Claude. For unknown model IDs,
OpenAnt defaults to `$0` per million tokens and prints a one-time warning
to stderr, so cost rollups stay honest rather than guessing. To plug in
real OpenRouter pricing, set `MODEL_PRICING_OVERRIDE` to a JSON object of
`{model_id: {input, output}}` per million tokens:

```bash
export MODEL_PRICING_OVERRIDE='{"qwen/qwen-3-coder-480b": {"input": 0.4, "output": 1.6}}'
```

Override values take precedence over the built-in table, so you can also
use this to update Claude pricing without code changes.

---

## Supported Vulnerabilities

| Type | Detection Pattern | Languages |
Expand Down
7 changes: 5 additions & 2 deletions libs/openant-core/core/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,11 @@ def run_analysis(
checkpoint = StepCheckpoint("Analyze", output_dir)
checkpoint.dir = checkpoint_path

# Select model
model_id = "claude-opus-4-6" if model == "opus" else "claude-sonnet-4-20250514"
# Select model. resolve_model_id() handles "opus"/"sonnet" aliases,
# passes through slash-form IDs verbatim, and strips a leading
# "openrouter/" prefix so OpenCode-style IDs work (see issue #9).
from utilities.llm_client import resolve_model_id
model_id = resolve_model_id(model)
print(f"[Analyze] Model: {model_id}", file=sys.stderr)

# Initialize client
Expand Down
6 changes: 5 additions & 1 deletion libs/openant-core/core/enhancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ def enhance_dataset(
# Configure global rate limiter
configure_rate_limiter(backoff_seconds=float(backoff_seconds))

model_id = "claude-sonnet-4-20250514" if model == "sonnet" else "claude-opus-4-6"
# resolve_model_id() handles "opus"/"sonnet" aliases, passes through
# slash-form IDs verbatim, and strips a leading "openrouter/" prefix
# so OpenCode-style IDs work (see issue #9).
from utilities.llm_client import resolve_model_id
model_id = resolve_model_id(model)
print(f"[Enhance] Mode: {mode}", file=sys.stderr)
print(f"[Enhance] Model: {model_id}", file=sys.stderr)

Expand Down
13 changes: 8 additions & 5 deletions libs/openant-core/generate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
import os
from datetime import datetime

import anthropic
from dotenv import load_dotenv

from utilities.llm_client import get_anthropic_client

# Load environment variables from .env file
load_dotenv()

Expand Down Expand Up @@ -198,11 +199,13 @@ def generate_remediation_guidance(findings: list) -> str:
{findings_text}
"""

api_key = os.getenv("ANTHROPIC_API_KEY")
if not api_key:
raise ValueError("ANTHROPIC_API_KEY not found in environment")
if not os.getenv("ANTHROPIC_API_KEY") and not os.getenv("OPENANT_LLM_API_KEY"):
raise ValueError(
"No API key found. Set ANTHROPIC_API_KEY, or for non-Claude "
"providers set OPENANT_LLM_API_KEY (and OPENANT_LLM_BASE_URL)."
)

client = anthropic.Anthropic(api_key=api_key)
client = get_anthropic_client()
response = client.messages.create(
model=REPORT_MODEL,
max_tokens=MAX_TOKENS,
Expand Down
30 changes: 25 additions & 5 deletions libs/openant-core/openant/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,10 +587,9 @@ def cmd_report_data(args):
and step reports — everything display-ready.
"""
import html as html_mod
import anthropic
from core.schemas import success, error
from core.step_report import step_context
from utilities.llm_client import get_global_tracker
from utilities.llm_client import get_anthropic_client, get_global_tracker

results_path = args.results
dataset_path = args.dataset
Expand Down Expand Up @@ -810,7 +809,7 @@ def cmd_report_data(args):
{findings_text}
"""
print("[Report] Generating remediation guidance (LLM)...", file=sys.stderr)
client = anthropic.Anthropic()
client = get_anthropic_client()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
Expand Down Expand Up @@ -985,7 +984,18 @@ def main():
help="Enable Docker-isolated dynamic testing (off by default)")
scan_p.add_argument("--no-skip-tests", action="store_true", help="Include test files in parsing (default: tests are skipped)")
scan_p.add_argument("--limit", type=int, help="Max units to analyze")
scan_p.add_argument("--model", choices=["opus", "sonnet"], default="opus", help="Model (default: opus)")
scan_p.add_argument(
"--model",
default="opus",
help=(
"Model to use. Accepts 'opus' / 'sonnet' aliases, an explicit "
"Claude ID, or a slash-form ID for an OpenAI/OpenRouter-compatible "
"endpoint (e.g. 'qwen/qwen-3-coder-480b'). The 'openrouter/' "
"prefix is recognised and stripped (e.g. "
"'openrouter/moonshotai/kimi-k2'). Set OPENANT_LLM_BASE_URL + "
"OPENANT_LLM_API_KEY to route requests to a non-Anthropic provider."
),
)
scan_p.add_argument("--workers", type=int, default=8,
help="Number of parallel workers for LLM steps (default: 8)")
scan_p.add_argument("--repo-name", help="Repository name (org/repo)")
Expand Down Expand Up @@ -1056,7 +1066,17 @@ def main():
help="Analyze units classified as exploitable or vulnerable_internal (safer, compensates for parser gaps)")
exploit_group.add_argument("--exploitable-only", action="store_true",
help="Analyze only units classified as exploitable (strict, use after parser entry point fixes)")
analyze_p.add_argument("--model", choices=["opus", "sonnet"], default="opus", help="Model (default: opus)")
analyze_p.add_argument(
"--model",
default="opus",
help=(
"Model to use. Accepts 'opus' / 'sonnet' aliases, an explicit "
"Claude ID, or a slash-form ID for an OpenAI/OpenRouter-compatible "
"endpoint (e.g. 'qwen/qwen-3-coder-480b'). The 'openrouter/' "
"prefix is recognised and stripped. Set OPENANT_LLM_BASE_URL + "
"OPENANT_LLM_API_KEY to route requests to a non-Anthropic provider."
),
)
analyze_p.add_argument("--workers", type=int, default=8,
help="Number of parallel workers for LLM calls (default: 8)")
analyze_p.add_argument("--checkpoint", help="Path to checkpoint directory for save/resume")
Expand Down
40 changes: 22 additions & 18 deletions libs/openant-core/report/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,28 @@
import os
import re
import sys
import anthropic
import anthropic # noqa: F401 — re-exported so monkeypatch tests can patch generator.anthropic.Anthropic
from pathlib import Path
from dotenv import load_dotenv

from .schema import validate_pipeline_output, ValidationError
from utilities.llm_client import get_anthropic_client, get_pricing

load_dotenv()

PROMPTS_DIR = Path(__file__).parent / "prompts"
MODEL = "claude-opus-4-6"

# Pricing per million tokens
_PRICING = {
"claude-opus-4-6": {"input": 15.00, "output": 75.00},
"claude-opus-4-20250514": {"input": 15.00, "output": 75.00},
"claude-sonnet-4-20250514": {"input": 3.00, "output": 15.00},
}
_DEFAULT_PRICING = {"input": 3.00, "output": 15.00}


def _extract_usage(response, model: str = MODEL) -> dict:
"""Extract usage info from an Anthropic API response."""
"""Extract usage info from an Anthropic API response.

Pricing is sourced from utilities.llm_client.get_pricing(), which
honours MODEL_PRICING_OVERRIDE and reports $0 (with a one-time stderr
warning) for unknown model IDs rather than guessing.
"""
usage = response.usage
pricing = _PRICING.get(model, _DEFAULT_PRICING)
pricing = get_pricing(model)
input_cost = (usage.input_tokens / 1_000_000) * pricing["input"]
output_cost = (usage.output_tokens / 1_000_000) * pricing["output"]
return {
Expand All @@ -54,11 +52,17 @@ def _merge_usage(usages: list[dict]) -> dict:


def _check_api_key():
"""Check that ANTHROPIC_API_KEY is set."""
if not os.environ.get("ANTHROPIC_API_KEY"):
print("Error: ANTHROPIC_API_KEY environment variable not set.", file=sys.stderr)
print("Set it with: export ANTHROPIC_API_KEY=sk-ant-...", file=sys.stderr)
sys.exit(1)
"""Check that an API key is set (ANTHROPIC_API_KEY or OPENANT_LLM_API_KEY)."""
if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENANT_LLM_API_KEY"):
return
print("Error: no API key found.", file=sys.stderr)
print("Set ANTHROPIC_API_KEY=sk-ant-... for Claude, or", file=sys.stderr)
print(
"OPENANT_LLM_API_KEY + OPENANT_LLM_BASE_URL for non-Claude providers "
"(see README).",
file=sys.stderr,
)
sys.exit(1)


def load_prompt(name: str) -> str:
Expand Down Expand Up @@ -136,7 +140,7 @@ def generate_summary_report(pipeline_data: dict) -> tuple[str, dict]:
output_tokens, total_tokens, cost_usd.
"""
_check_api_key()
client = anthropic.Anthropic()
client = get_anthropic_client()

summary_data = _compact_for_summary(pipeline_data)
system_prompt = load_prompt("system")
Expand Down Expand Up @@ -199,7 +203,7 @@ def generate_disclosure(vulnerability_data: dict, product_name: str) -> tuple[st
(disclosure_text, usage_dict)
"""
_check_api_key()
client = anthropic.Anthropic()
client = get_anthropic_client()

system_prompt = load_prompt("system")

Expand Down
Loading