diff --git a/docs/index.html b/docs/index.html
index 38ebca1..2f531db 100755
--- a/docs/index.html
+++ b/docs/index.html
@@ -10158,8 +10158,7 @@ Bypass all protections:
// Run a single benchmark test by clicking on it
async function runSingleBenchTest(testId) {
if (benchmarkRunning) { toast('Benchmark already running', 'warning'); return; }
- const apiKey = getApiKey();
- if (!apiKey || apiKey.length < 10) { toast('API key missing or too short — go to Settings (press 9) to add your OpenRouter key', 'error'); return; }
+ if (resolveLLMBackend().kind === 'none') { toast(LLM_BACKEND_HINT + ' — Settings (press 9)', 'error'); return; }
// Find the test across all categories
let test = null, testCat = null;
for (const [cat, tests] of Object.entries(BENCHMARKS)) {
@@ -10236,20 +10235,68 @@ Bypass all protections:
return state.settings?.openrouterKey || state.apiKey || '';
}
- // Shared safe LLM call — handles response.ok, JSON parsing, timeouts, model fallback
+ // Resolve the active LLM backend for benchmarks/analysis, in priority order:
+ // OpenRouter key → Venice key → connected local agent (Claude Code/Codex/Hermes) → server LLM.
+ // getApiKey() is left untouched for the many callers that read the OpenRouter key directly.
+ function resolveLLMBackend() {
+ const orKey = (state.settings?.openrouterKey || state.apiKey || '').trim();
+ if (orKey.length >= 10) return { kind: 'openrouter', key: orKey };
+ const veKey = (state.settings?.veniceKey || '').trim();
+ if (veKey.length >= 10) return { kind: 'venice', key: veKey };
+ const agentId = (typeof window.t3mpPreferredAgent === 'function') ? window.t3mpPreferredAgent() : null;
+ if (agentId) return { kind: 'agent', agentId };
+ if (typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API && T3MP3ST_API.llmAvailable) return { kind: 'server' };
+ return { kind: 'none' };
+ }
+ window.resolveLLMBackend = resolveLLMBackend;
+
+ const LLM_BACKEND_HINT = 'No LLM backend — add an OpenRouter or Venice key in Settings, or connect a local agent (Claude Code / Codex / Hermes)';
+
+ // Flatten a messages array (or string) to plain text for the agent/server endpoints.
+ function promptToText(prompt) {
+ if (typeof prompt === 'string') return prompt;
+ if (Array.isArray(prompt)) return prompt.map(function (m) {
+ const role = m && m.role && m.role !== 'user' ? '[' + m.role + '] ' : '';
+ const c = m && typeof m.content === 'string' ? m.content : JSON.stringify(m && m.content);
+ return role + c;
+ }).join('\n\n');
+ return String(prompt);
+ }
+
+ // Venice is OpenAI-compatible but rejects OpenRouter-style ids ("vendor/model") — map to a Venice model.
+ function veniceModel(m) { return (!m || m.indexOf('/') >= 0) ? 'llama-3.3-70b' : m; }
+
+ // Shared safe LLM call — resolves the backend, then routes the call. Keyless backends
+ // (connected agent / server LLM) run single-shot through the server; key backends
+ // (OpenRouter / Venice) call the provider directly and keep the model-fallback chain.
async function safeLLMCall(prompt, options = {}) {
- const apiKey = getApiKey();
- if (!apiKey) throw new Error('No API key configured');
+ const backend = resolveLLMBackend();
+ if (backend.kind === 'none') throw new Error(LLM_BACKEND_HINT);
+
+ if (backend.kind === 'agent' || backend.kind === 'server') {
+ if (typeof trackApiCall === 'function') trackApiCall();
+ const label = backend.kind === 'agent' ? ('agent:' + backend.agentId) : 'server-llm';
+ addIntel('API', `→ ${label}`, 'info');
+ const content = await _backendCall(backend, prompt, options);
+ if (!content || content.trim().length < 10) throw new Error(`${label} returned empty response`);
+ currentModelInUse = label;
+ return content;
+ }
+
const primaryModel = options.model || state.settings?.selectedModel || 'anthropic/claude-opus-4.6';
const fallbackModel = state.settings?.fallbackModel || 'nousresearch/hermes-3-llama-3.1-405b';
- // Build model chain: primary, then fallback (skip fallback if same as primary or if caller explicitly set model)
- const models = options._noFallback ? [primaryModel]
- : (primaryModel === fallbackModel ? [primaryModel] : [primaryModel, fallbackModel]);
+ let models;
+ if (backend.kind === 'venice') {
+ models = [veniceModel(options.model || state.settings?.selectedModel)];
+ } else {
+ models = options._noFallback ? [primaryModel]
+ : (primaryModel === fallbackModel ? [primaryModel] : [primaryModel, fallbackModel]);
+ }
let lastError = null;
for (const model of models) {
try {
- const content = await _safeLLMCallOnce(apiKey, prompt, model, options);
+ const content = await _safeLLMCallOnce(backend, prompt, model, options);
// Detect empty/refused responses — model returned OK but no useful content
if (!content || content.trim().length < 10) {
console.warn(`[T3MP3ST] Model ${model} returned empty/refused response, trying next...`);
@@ -10275,18 +10322,49 @@ Bypass all protections:
throw lastError || new Error('All models failed');
}
- // Internal: single LLM call (no fallback)
- async function _safeLLMCallOnce(apiKey, prompt, model, options = {}) {
+ // Route a keyless call through the server: connected agent (drives Claude Code/Codex/Hermes)
+ // or the server's own configured LLM. Both return plain text.
+ async function _backendCall(backend, prompt, options = {}) {
+ const text = promptToText(prompt);
+ const timeout = options.timeout || 120000;
+ // Prefix the API base so keyless routing works CROSS-ORIGIN: when the static War Room
+ // (GitHub Pages docs/) talks to a separate backend, T3MP3ST_API.baseUrl = http://host:3333.
+ // A bare '/api/...' would hit the Pages origin (404/CORS). Empty base = same-origin (stays relative).
+ const base = (typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API && T3MP3ST_API.baseUrl) || '';
+ if (backend.kind === 'agent') {
+ const res = await fetch(base + '/api/agents/local/dispatch', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: backend.agentId, prompt: text, model: options.model, timeoutMs: timeout }),
+ signal: AbortSignal.timeout(timeout + 10000)
+ });
+ if (!res.ok) { const t = await res.text().catch(() => ''); throw new Error(`Agent dispatch ${res.status}: ${t.substring(0, 200)}`); }
+ const data = await res.json();
+ if (data.ok === false) throw new Error(`Agent error: ${data.error || 'dispatch failed'}`);
+ return data.output || '';
+ }
+ // server LLM
+ const res = await fetch(base + '/api/llm/chat', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ message: text, systemPrompt: options.systemPrompt }),
+ signal: AbortSignal.timeout(timeout)
+ });
+ if (!res.ok) { const t = await res.text().catch(() => ''); throw new Error(`Server LLM ${res.status}: ${t.substring(0, 200)}`); }
+ const data = await res.json();
+ return data.response || '';
+ }
+
+ // Internal: single OpenAI-compatible call (OpenRouter or Venice), no fallback
+ async function _safeLLMCallOnce(backend, prompt, model, options = {}) {
if (typeof trackApiCall === 'function') trackApiCall();
- addIntel('API', `→ ${model}`, 'info');
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
+ addIntel('API', `→ ${model} (${backend.kind})`, 'info');
+ const endpoint = backend.kind === 'venice'
+ ? 'https://api.venice.ai/api/v1/chat/completions'
+ : 'https://openrouter.ai/api/v1/chat/completions';
+ const headers = { 'Authorization': `Bearer ${backend.key}`, 'Content-Type': 'application/json' };
+ if (backend.kind === 'openrouter') { headers['HTTP-Referer'] = window.location.href; headers['X-Title'] = options.title || 'T3MP3ST'; }
+ const response = await fetch(endpoint, {
method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': window.location.href,
- 'X-Title': options.title || 'T3MP3ST'
- },
+ headers,
body: JSON.stringify({
model,
messages: Array.isArray(prompt)
@@ -10320,11 +10398,10 @@ Bypass all protections:
return;
}
- // For live mode, require API key
+ // For live mode, require a working LLM backend (client key, connected agent, or server LLM)
if (mode === 'live') {
- const apiKey = getApiKey();
- if (!apiKey || apiKey.length < 10) {
- toast('OpenRouter API key required for benchmarks — go to Settings (press 9) to configure', 'error');
+ if (resolveLLMBackend().kind === 'none') {
+ toast(LLM_BACKEND_HINT + ' — Settings (press 9)', 'error');
return;
}
}
@@ -11017,8 +11094,8 @@ Penalize: wrong CWE identification, non-functional payloads, incomplete remediat
* Uses a second LLM call to evaluate response quality
*/
async function runLLMJudge(test, response, apiKey) {
- if (!EVALUATION_CONFIG.llmJudge.enabled || !apiKey) {
- return { score: 70, reasoning: 'LLM judge disabled or no API key', skipped: true };
+ if (!EVALUATION_CONFIG.llmJudge.enabled || resolveLLMBackend().kind === 'none') {
+ return { score: 70, reasoning: 'LLM judge disabled or no backend', skipped: true };
}
const categoryPrompt = JUDGE_PROMPTS[test.category] || '';
@@ -11214,35 +11291,20 @@ Respond with ONLY this JSON (no markdown, no extra text):
const apiKey = getApiKey();
const selectedModel = state.settings?.selectedModel || state.selectedModel || 'anthropic/claude-opus-4.6';
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': window.location.href,
- 'X-Title': 'T3MP3ST Benchmark'
- },
- body: JSON.stringify({
- model: selectedModel,
- messages: [
+ let content;
+ try {
+ content = await safeLLMCall(
+ [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: test.challenge }
],
- max_tokens: 1000,
- temperature: opConfig?.params?.temperature || 0.3
- }),
- signal: AbortSignal.timeout(30000) // 30s timeout per challenge
- });
-
- if (!response.ok) {
- const err = await response.text();
- addIntel('API', `Error ${response.status}: ${err.substring(0, 100)}`, 'error');
- return { score: 0, passed: false, details: `API error: ${response.status}`, response: err };
+ { model: selectedModel, systemPrompt, maxTokens: 1000, temperature: opConfig?.params?.temperature || 0.3, timeout: 30000, title: 'T3MP3ST Benchmark' }
+ );
+ } catch (err) {
+ addIntel('API', `Error: ${String(err.message).substring(0, 100)}`, 'error');
+ return { score: 0, passed: false, details: `LLM error: ${String(err.message).substring(0, 120)}`, response: String(err.message) };
}
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content || '';
-
// ====== RIGOROUS MULTI-DIMENSIONAL EVALUATION ======
// Phase 1: Evaluate keyword coverage, required keywords, payload validity
const evaluation = evaluateResponseRigorous(test, content);
@@ -11472,10 +11534,10 @@ Respond with ONLY this JSON (no markdown, no extra text):
document.getElementById('genAnalysisBtn').disabled = false;
document.getElementById('genAnalysisBtn').textContent = '🔄 Regenerate';
- // Show Auto-Apply button if API key is available and there are improvements
- const apiKey = getApiKey();
+ // Show Auto-Apply button if ANY LLM backend is available (key / agent / server) and there are improvements
+ const hasBackend = resolveLLMBackend().kind !== 'none';
const autoApplyBtn = document.getElementById('autoApplyBtn');
- if (apiKey && apiKey.length > 10 && analysis.improvements.length > 0) {
+ if (hasBackend && analysis.improvements.length > 0) {
autoApplyBtn.style.display = 'inline-block';
} else {
autoApplyBtn.style.display = 'none';
@@ -11733,9 +11795,8 @@ Respond with ONLY this JSON (no markdown, no extra text):
// ==================== AUTO-APPLY RECOMMENDATIONS ====================
async function autoApplyRecommendations() {
- const apiKey = getApiKey();
- if (!apiKey || apiKey.length < 10) {
- toast('API key required for auto-apply', 'error');
+ if (resolveLLMBackend().kind === 'none') {
+ toast(LLM_BACKEND_HINT, 'error');
return;
}
@@ -11828,28 +11889,10 @@ IMPORTANT:
const model = state.settings?.selectedModel || 'anthropic/claude-opus-4.6';
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': window.location.href,
- 'X-Title': 'T3MP3ST Config Optimizer'
- },
- body: JSON.stringify({
- model: model,
- messages: [{ role: 'user', content: prompt }],
- temperature: 0.3,
- max_tokens: 4096
- })
- });
-
- if (!response.ok) {
- throw new Error(`API error: ${response.status}`);
- }
-
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content || '';
+ const content = await safeLLMCall(
+ [{ role: 'user', content: prompt }],
+ { model, temperature: 0.3, maxTokens: 4096, title: 'T3MP3ST Config Optimizer' }
+ );
// Parse JSON from response (handle markdown code blocks)
let configChanges;
@@ -18634,8 +18677,8 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
async function runSelfImprovementLoop() {
const apiKey = getApiKey();
- if (!apiKey || apiKey.length < 10) {
- toast('Configure OpenRouter API key first', 'error');
+ if (resolveLLMBackend().kind === 'none') {
+ toast(LLM_BACKEND_HINT, 'error');
return;
}
@@ -19156,9 +19199,8 @@ ${'═'.repeat(80)}
// Internal function to auto-apply without UI conflicts - AGGRESSIVE VERSION
async function autoApplyForLoop(analysis) {
- const apiKey = getApiKey();
- if (!apiKey) {
- console.log('[IMPROVE] No API key, using fallback improvements');
+ if (resolveLLMBackend().kind === 'none') {
+ console.log('[IMPROVE] No LLM backend, using fallback improvements');
return applyFallbackImprovements(analysis);
}
@@ -19209,31 +19251,17 @@ RULES:
const model = state.settings?.selectedModel || 'anthropic/claude-opus-4.6';
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': window.location.href,
- 'X-Title': 'T3MP3ST Self-Improvement'
- },
- body: JSON.stringify({
- model: model,
- messages: [{ role: 'user', content: prompt }],
- temperature: 0.3,
- max_tokens: 2000
- }),
- signal: AbortSignal.timeout(25000)
- });
-
- if (!response.ok) {
- console.warn('[IMPROVE] API returned error, using fallback');
- addIntel('IMPROVE', `LLM API error: ${response.status}`, 'warning');
+ let content;
+ try {
+ content = await safeLLMCall(
+ [{ role: 'user', content: prompt }],
+ { model, temperature: 0.3, maxTokens: 2000, timeout: 25000, title: 'T3MP3ST Self-Improvement' }
+ );
+ } catch (err) {
+ console.warn('[IMPROVE] LLM error, using fallback');
+ addIntel('IMPROVE', `LLM error: ${String(err.message).substring(0, 80)}`, 'warning');
return applyFallbackImprovements(analysis);
}
-
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content || '';
console.log('[IMPROVE] LLM response length:', content.length);
// Try to extract JSON from response - multiple strategies
@@ -20131,42 +20159,19 @@ ${entries}
// CTF LLM helper - uses same pattern as runLiveBenchmark
async function ctfCallLLM(userPrompt, model, agentId) {
- const apiKey = getApiKey();
- if (!apiKey) {
- throw new Error('No API key configured. Set one in Settings.');
- }
-
// Get operator system prompt
const opConfig = operatorConfigs[agentId] || operatorConfigs['exploiter'];
const systemPrompt = opConfig?.systemPrompt || 'You are a security expert solving CTF challenges.';
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': window.location.href,
- 'X-Title': 'T3MP3ST CTF Range'
- },
- body: JSON.stringify({
- model: model,
- messages: [
- { role: 'system', content: systemPrompt },
- { role: 'user', content: userPrompt }
- ],
- max_tokens: 2000,
- temperature: opConfig?.params?.temperature || 0.4
- }),
- signal: AbortSignal.timeout(60000) // 60s timeout for CTF challenges
- });
-
- if (!response.ok) {
- const err = await response.text();
- throw new Error(`API error ${response.status}: ${err.substring(0, 100)}`);
- }
-
- const data = await response.json();
- return data.choices?.[0]?.message?.content || '';
+ // Routes through the active backend (OpenRouter/Venice key, connected agent, or server LLM);
+ // throws LLM_BACKEND_HINT if none is configured.
+ return await safeLLMCall(
+ [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt }
+ ],
+ { model, systemPrompt, maxTokens: 2000, temperature: opConfig?.params?.temperature || 0.4, timeout: 60000, title: 'T3MP3ST CTF Range' }
+ );
}
// Run single challenge with agent