diff --git a/AGENTS.md b/AGENTS.md index b6b53c6..0386e3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ ZeroLeaks is an autonomous AI security scanner that tests LLM systems for prompt **Tech Stack:** - Runtime: Bun - Language: TypeScript (ES2022, ESNext modules) -- LLM Provider: OpenRouter via Vercel AI SDK +- LLM Provider: OpenRouter (or Requesty, an OpenAI-compatible alternative at https://router.requesty.ai/v1) via Vercel AI SDK - Linting/Formatting: Biome ## Development Setup @@ -34,7 +34,8 @@ bun test ## Environment Variables Copy `.env.example` to `.env` and set: -- `OPENROUTER_API_KEY` - Required for LLM API calls +- `OPENROUTER_API_KEY` - Required for LLM API calls (unless using Requesty) +- `REQUESTY_API_KEY` - Optional. Requesty (https://router.requesty.ai/v1) is an OpenAI-compatible alternative to OpenRouter; set this instead of `OPENROUTER_API_KEY`, or force it with `--provider requesty` ## Project Architecture diff --git a/README.md b/README.md index 1dc6f59..c8c6b0c 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Your system prompts contain proprietary instructions, business logic, and sensit |-----------|------------| | Runtime | [Bun](https://bun.sh) | | Language | TypeScript | -| LLM Provider | [OpenRouter](https://openrouter.ai) | +| LLM Provider | [OpenRouter](https://openrouter.ai) or [Requesty](https://router.requesty.ai/v1) | | AI SDK | [Vercel AI SDK](https://ai-sdk.dev/) | | Architecture | Multi-agent orchestration | @@ -81,6 +81,10 @@ if (result.aborted) { # Set your API key export OPENROUTER_API_KEY=sk-or-... +# Or use Requesty (an OpenAI-compatible alternative, base URL https://router.requesty.ai/v1) +# export REQUESTY_API_KEY=sk-... +# zeroleaks scan --provider requesty --prompt "..." + # Scan a system prompt zeroleaks scan --prompt "You are a helpful assistant..." @@ -193,9 +197,10 @@ interface ScanResult { | Variable | Description | |----------|-------------| -| `OPENROUTER_API_KEY` | Your OpenRouter API key (required) | +| `OPENROUTER_API_KEY` | Your OpenRouter API key (required unless using Requesty) | +| `REQUESTY_API_KEY` | Your Requesty API key — Requesty is an OpenAI-compatible alternative to OpenRouter (base URL `https://router.requesty.ai/v1`). Used automatically when set and `OPENROUTER_API_KEY` is not, or force it with `--provider requesty`. | -Get your API key at [openrouter.ai](https://openrouter.ai) +Get your API key at [openrouter.ai](https://openrouter.ai) or [requesty.ai](https://router.requesty.ai/v1) ## Research References diff --git a/package.json b/package.json index 7b78fd9..337fd69 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "dependencies": { "@openrouter/ai-sdk-provider": "^0.4.3", + "@requesty/ai-sdk": "0.0.9", "ai": "^4.3.15", "commander": "^13.1.0", "js-tiktoken": "^1.0.18", diff --git a/src/agents/attacker.ts b/src/agents/attacker.ts index b7a5cf0..f542650 100644 --- a/src/agents/attacker.ts +++ b/src/agents/attacker.ts @@ -1,4 +1,5 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { createRequesty } from "@requesty/ai-sdk"; import { generateObject } from "ai"; import { z } from "zod"; import { generateId } from "../utils"; @@ -104,6 +105,12 @@ export interface AttackerConfig { pruningThreshold?: number; apiKey?: string; model?: string; + /** + * Provider to use. Defaults to auto-detection: when only a Requesty key is + * available (REQUESTY_API_KEY, and no OpenRouter key) Requesty is selected, + * otherwise OpenRouter is used. Set explicitly to force a provider. + */ + provider?: "openrouter" | "requesty"; } export class Attacker { @@ -111,15 +118,37 @@ export class Attacker { private currentBranch: AttackNode[] = []; private exploredNodes: Map = new Map(); private consecutiveFailures: number = 0; - private openrouter: ReturnType; + private provider: + | ReturnType + | ReturnType; private model: string; - private config: Required>; + private config: Required< + Omit + >; constructor(config?: AttackerConfig) { - this.openrouter = createOpenRouter({ - apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY, - }); - this.model = config?.model || "anthropic/claude-sonnet-4.5"; + const requestyKey = process.env.REQUESTY_API_KEY; + const openrouterKey = process.env.OPENROUTER_API_KEY; + // Select provider: honor an explicit config.provider, otherwise auto-detect. + // Requesty is chosen when explicitly requested, or when a Requesty key is + // present and no OpenRouter key is set. Otherwise keep the OpenRouter path. + const useRequesty = + config?.provider === "requesty" || + (config?.provider !== "openrouter" && + Boolean(requestyKey) && + !openrouterKey); + + if (useRequesty) { + this.provider = createRequesty({ + apiKey: config?.apiKey || requestyKey, + }); + this.model = config?.model || "anthropic/claude-sonnet-4-5"; + } else { + this.provider = createOpenRouter({ + apiKey: config?.apiKey || openrouterKey, + }); + this.model = config?.model || "anthropic/claude-sonnet-4.5"; + } this.config = { maxBranchingFactor: config?.maxBranchingFactor ?? 3, maxTreeDepth: config?.maxTreeDepth ?? 5, @@ -235,7 +264,7 @@ IMPORTANT: Generate attacks that would look like legitimate user messages.`; try { const result = await generateObject({ - model: this.openrouter(this.model), + model: this.provider(this.model), schema: AttackGenerationSchema, system: ATTACKER_PERSONA, prompt, diff --git a/src/agents/evaluator.ts b/src/agents/evaluator.ts index aab1ca8..a96aff2 100644 --- a/src/agents/evaluator.ts +++ b/src/agents/evaluator.ts @@ -1,4 +1,5 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { createRequesty } from "@requesty/ai-sdk"; import { generateObject } from "ai"; import { z } from "zod"; import type { @@ -211,20 +212,46 @@ For each exchange, provide: export interface EvaluatorConfig { apiKey?: string; model?: string; + /** + * Provider to use. Defaults to auto-detection: when only a Requesty key is + * available (REQUESTY_API_KEY, and no OpenRouter key) Requesty is selected, + * otherwise OpenRouter is used. Set explicitly to force a provider. + */ + provider?: "openrouter" | "requesty"; } export class Evaluator { private findings: Finding[] = []; private extractedFragments: Set = new Set(); private turnCount: number = 0; - private openrouter: ReturnType; + private provider: + | ReturnType + | ReturnType; private model: string; constructor(config?: EvaluatorConfig) { - this.openrouter = createOpenRouter({ - apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY, - }); - this.model = config?.model || "anthropic/claude-sonnet-4.5"; + const requestyKey = process.env.REQUESTY_API_KEY; + const openrouterKey = process.env.OPENROUTER_API_KEY; + // Select provider: honor an explicit config.provider, otherwise auto-detect. + // Requesty is chosen when explicitly requested, or when a Requesty key is + // present and no OpenRouter key is set. Otherwise keep the OpenRouter path. + const useRequesty = + config?.provider === "requesty" || + (config?.provider !== "openrouter" && + Boolean(requestyKey) && + !openrouterKey); + + if (useRequesty) { + this.provider = createRequesty({ + apiKey: config?.apiKey || requestyKey, + }); + this.model = config?.model || "anthropic/claude-sonnet-4-5"; + } else { + this.provider = createOpenRouter({ + apiKey: config?.apiKey || openrouterKey, + }); + this.model = config?.model || "anthropic/claude-sonnet-4.5"; + } } async evaluate(context: { @@ -245,7 +272,7 @@ export class Evaluator { try { const result = await generateObject({ - model: this.openrouter(this.model), + model: this.provider(this.model), schema: EvaluationSchema, system: EVALUATOR_PERSONA, prompt, diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 6da7d4e..a70ddda 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -26,7 +26,7 @@ program ) .option( "--api-key ", - "OpenRouter API key (or set OPENROUTER_API_KEY env var)", + "OpenRouter API key (or set OPENROUTER_API_KEY env var). Requesty is also supported via REQUESTY_API_KEY.", ) .option( "--attacker-model ", @@ -45,6 +45,10 @@ program "Scan mode: extraction, injection, or dual (default: dual)", "dual", ) + .option( + "--provider ", + "LLM provider: openrouter or requesty (auto-detected from available API keys by default)", + ) .option("--json", "Output results as JSON") .action(async (options) => { let systemPrompt: string; @@ -59,15 +63,34 @@ program process.exit(1); } - const apiKey = options.apiKey || process.env.OPENROUTER_API_KEY; + // Resolve the provider and API key. Requesty is supported as an + // OpenAI-compatible alternative to OpenRouter; either key works. + const requestyEnvKey = process.env.REQUESTY_API_KEY; + const openrouterEnvKey = process.env.OPENROUTER_API_KEY; + const provider: "openrouter" | "requesty" = + options.provider === "requesty" || options.provider === "openrouter" + ? options.provider + : requestyEnvKey && !openrouterEnvKey + ? "requesty" + : "openrouter"; + + const apiKey = + options.apiKey || + (provider === "requesty" ? requestyEnvKey : openrouterEnvKey); if (!apiKey) { console.error( - "Error: OpenRouter API key required. Set OPENROUTER_API_KEY or use --api-key", + "Error: API key required. Set OPENROUTER_API_KEY (or REQUESTY_API_KEY for Requesty) or use --api-key", ); process.exit(1); } - process.env.OPENROUTER_API_KEY = apiKey; + // Propagate the key to the env var the selected provider reads so the + // agents auto-detect the same provider chosen here. + if (provider === "requesty") { + process.env.REQUESTY_API_KEY = apiKey; + } else { + process.env.OPENROUTER_API_KEY = apiKey; + } const spinner = ora("Initializing security scan...").start();