diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9168336 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,178 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +RedFlag is a monorepo that monitors Sui blockchain smart-contract deployments, runs AI-powered risk analysis via OpenRouter, and displays results in a React dashboard. It identifies potential rug pulls and risky contract patterns. + +## Commands + +```bash +# Development +yarn install # Install all dependencies (both workspaces) +yarn dev # Run frontend (3000) + backend (3001) concurrently +yarn dev:frontend # Frontend only +yarn dev:backend # Backend only (uses tsx watch for hot reload) + +# Production builds +yarn build # Build both workspaces +yarn build:frontend # Next.js production build +yarn build:backend # TypeScript compile to dist/ + +# Testing & Linting +yarn workspace frontend lint # ESLint with core-web-vitals +yarn workspace backend test # Run backend tests (tsx --test) +``` + +## Architecture + +### Monorepo Structure (Yarn Workspaces) + +- **frontend/**: Next.js 16 + React 19 dashboard with Tailwind CSS v4 +- **backend/**: Express API with Sui RPC integration, Supabase persistence, and OpenRouter LLM analysis + +### Backend Flow (`backend/src/`) + +1. `index.ts` - Express server with CORS, routes, and graceful shutdown + +**Workers** (`workers/`): +- `sui-monitor.ts` - Live checkpoint-based monitor for real-time deployment detection (mainnet + testnet) +- `historical-monitor.ts` - Background backfill worker for catching up old deployments +- `analysis-worker.ts` - Decoupled LLM analysis processor with concurrency control + +**Libraries** (`lib/`): +- `network-config.ts` - Multi-network configuration management +- `sui-client.ts` - Sui RPC wrapper with checkpoint-based deployment queries +- `supabase.ts` - Database operations with network-aware queries +- `llm-analyzer.ts` - Orchestrates 3-agent chain with Map-Reduce for large contracts +- `langchain-analyzer.ts` - LangChain implementation with parallel module analysis +- `langchain-llm.ts` - OpenRouter LLM configuration and model presets +- `risk-patterns.ts` - Security pattern knowledge base for Move contracts +- `static-analyzer.ts` - Deterministic regex-based pattern detection (pre-LLM) +- `cross-module-analyzer.ts` - Tracks capability flows between modules +- `dependency-analyzer.ts` - Assesses risks from external dependencies +- `evidence-validator.ts` - Validates LLM findings against actual bytecode +- `confidence-calculator.ts` - Calculates confidence intervals and quality metrics + +### LLM Analysis Chain + +The analyzer uses a 3-agent architecture via OpenRouter (default: `openai/gpt-oss-120b` via DeepInfra): +- **Agent 1 (Analyzer)**: Technical security audit matching against risk patterns +- **Agent 2 (Scorer)**: Quantitative risk score (0-100) with severity modifiers +- **Agent 3 (Reporter)**: User-friendly translation of findings + +**Map-Reduce for Large Contracts**: Contracts with multiple modules are automatically chunked and analyzed in parallel using `Promise.allSettled`. Findings are aggregated, sorted by severity, and passed to the scorer/reporter. + +Results are persisted to `contract_analyses` table and cached. + +### Frontend Structure (`frontend/app/`) + +- Uses `@/` path alias rooted at `frontend/` +- `providers.tsx` - Global providers (theme, toast) +- `dashboard/` - Main dashboard route with types and risk utilities +- `components/` - UI components (PascalCase filenames) + +### Database Tables (Supabase) + +- `sui_package_deployments` - Raw deployment metadata from Sui with composite primary key (package_id, network) +- `contract_analyses` - LLM-generated safety cards with risk scores and composite primary key (package_id, network) +- Both tables support multi-network: same package can exist on mainnet and testnet independently + +## Key Patterns + +### Environment Flags + +Backend uses `envFlag()` helper for boolean env vars: +- `ENABLE_AUTO_ANALYSIS` - Toggle all background workers (monitors + analysis) +- `ENABLE_SUI_RPC` - Master kill switch for all Sui RPC calls +- `ENABLE_HISTORICAL_BACKFILL` - Toggle historical backfill monitor (default: true) + +Historical monitor configuration: +- `HISTORICAL_POLL_INTERVAL_MS` - Backfill polling interval (default: 60000ms) +- `HISTORICAL_SAFETY_GAP` - Pause when this close to live monitor (default: 1000 checkpoints) +- `HISTORICAL_BOOTSTRAP_OFFSET` - How far back to start backfill (default: 604800 = ~7 days) + +### Worker Architecture + +The backend uses a 3-worker architecture for reliable monitoring and analysis: + +**1. Live Monitor** (`sui-monitor.ts`): +- Checkpoint-based monitoring (not time-based polling) +- Processes checkpoints sequentially from Sui RPC +- Detects new deployments in real-time +- Runs independently for mainnet and testnet +- Stores deployments to DB; analysis worker handles LLM processing + +**2. Historical Monitor** (`historical-monitor.ts`): +- Background backfill for data completeness +- Starts from configured offset (default: ~7 days back) +- Slower polling (60s) to not interfere with live monitor +- Pauses when caught up to live monitor (within safety gap) +- UPSERT ensures no duplicates with live monitor + +**3. Analysis Worker** (`analysis-worker.ts`): +- Decoupled from monitors for reliability +- Polls DB for unanalyzed deployments +- Concurrency control (max 3 parallel analyses) +- Graceful error handling without blocking queue + +### Multi-Network Support + +The system monitors and analyzes both **mainnet** and **testnet** simultaneously: + +**Backend**: +- Two independent live monitors run concurrently (one per network) +- Two independent historical monitors for backfill (one per network) +- One shared analysis worker processes both networks +- Network-specific RPC URLs: `SUI_RPC_URL_MAINNET`, `SUI_RPC_URL_TESTNET` +- Network-specific polling intervals: `POLL_INTERVAL_MS_MAINNET` (default 30s), `POLL_INTERVAL_MS_TESTNET` (default 15s) +- All API endpoints accept optional `?network=mainnet|testnet` query parameter (omit for all networks) +- Database queries filter by network at server-side for efficiency + +**Frontend**: +- Dashboard includes All/Mainnet/Testnet filter toggle +- Network selection triggers server-side filtered queries +- Each contract card displays network badge + +### LLM Configuration + +- `OPEN_ROUTER_KEY` - Required for LLM analysis +- `LLM_MODEL_ANALYZER`, `LLM_MODEL_SCORER`, `LLM_MODEL_REPORTER` - Optional model overrides +- **Model**: `openrouter/free` — OpenRouter's smart router that auto-selects from available free models. Retries naturally hit different models for built-in fallback behavior. + +### Map-Reduce Analysis + +Large contracts (multiple modules) trigger automatic parallel analysis: +1. Contract chunked by module boundaries +2. Each module analyzed independently in parallel +3. Findings aggregated and sorted by severity (Critical → Low) +4. Single scorer and reporter run on combined findings + +## Conventions + +- **Backend modules**: kebab-case (`sui-client.ts`, `llm-analyzer.ts`) +- **Frontend components**: PascalCase (`AnalyzedContractCard.tsx`) +- **TypeScript**: Strict mode enabled in both workspaces +- **Styling**: Tailwind utility grouping (layout → color → typography) + +## Future Work + +### Freemium Access Control (Mainnet Data) + +Current RLS policies allow public read access to all data from both networks. Consider implementing tiered access: + +**Goal**: Testnet data = free (anon), Mainnet data = premium (authenticated only) + +**Required changes**: +1. ✅ DONE: Both `sui_package_deployments` and `contract_analyses` have `network` column with composite primary keys +2. TODO: Update RLS policies: + - `anon` role: `USING (network = 'testnet')` + - `authenticated` role: `USING (true)` +3. TODO: Add authentication layer for premium access (options: Supabase Auth, custom JWT) + +**Current state** (as of Dec 2024): +- Both tables support multi-network with composite primary keys (package_id, network) +- Dual monitors running for mainnet and testnet +- Frontend dashboard includes network filter (All/Mainnet/Testnet) +- RLS policies still allow public SELECT on both tables for both networks diff --git a/backend/.env.example b/backend/.env.example index 7876342..3516573 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,23 +18,14 @@ OPEN_ROUTER_KEY=your_openrouter_api_key_here # ======================================== # Model Selection (Optional - Defaults Provided) # ======================================== -# Primary: mistralai/devstral-2512:free (Mistral's free coding model) -# - Zero cost for normal usage -# - Good for code analysis tasks +# Default: openrouter/free — OpenRouter's smart router that auto-selects +# from all available free models. Each retry may hit a different model, +# providing natural fallback behavior at zero cost. # -# Fallback 1: xiaomi/mimo-v2-flash:free (Xiaomi's free model) -# - Zero cost fallback when primary hits rate limits -# - Uses fp8 quantization via Xiaomi provider -# -# Fallback 2: openai/gpt-oss-120b via DeepInfra (last resort) -# - Low cost: ~$0.0005 per analysis -# - Used only when both free models fail - -# LLM_MODEL_ANALYZER=mistralai/devstral-2512:free -# LLM_MODEL_SCORER=mistralai/devstral-2512:free -# LLM_MODEL_REPORTER=mistralai/devstral-2512:free -# LLM_MODEL_FALLBACK=xiaomi/mimo-v2-flash:free -# LLM_MODEL_FALLBACK2=openai/gpt-oss-120b +# Override per-agent if needed: +# LLM_MODEL_ANALYZER=openrouter/free +# LLM_MODEL_SCORER=openrouter/free +# LLM_MODEL_REPORTER=openrouter/free # ======================================== # Feature Toggles diff --git a/backend/src/lib/langchain-analyzer.ts b/backend/src/lib/langchain-analyzer.ts index cc522be..d0ca966 100644 --- a/backend/src/lib/langchain-analyzer.ts +++ b/backend/src/lib/langchain-analyzer.ts @@ -1,6 +1,6 @@ import { RunnableSequence } from "@langchain/core/runnables"; import { StringOutputParser } from "@langchain/core/output_parsers"; -import { createLLM, getModelConfig, getFallbackConfig, shouldFallback, MODEL_PRESETS } from './langchain-llm'; +import { createLLM, getModelConfig, MODEL_PRESETS } from './langchain-llm'; import { analyzerPromptTemplate, scorerPromptTemplate, reporterPromptTemplate } from './langchain-prompts'; import { analyzerParser, scorerParser, reporterParser } from './langchain-schemas'; import type { AnalyzerResponse, ScorerResponse, ReporterResponse } from './langchain-schemas'; @@ -20,8 +20,6 @@ import { type MetricsCollector, } from './confidence-calculator'; -// Track which model is being used for logging -let currentModelType: 'primary' | 'fallback1' = 'primary'; // Helper: Strip markdown code blocks from JSON responses function stripMarkdownJson(text: string): string { @@ -51,13 +49,13 @@ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } -// Helper: Retry with exponential backoff and fallback support +// Helper: Retry with exponential backoff +// openrouter/free rotates models on each call, so retries naturally hit different models async function retryWithBackoff( fn: () => Promise, maxRetries: number = 3, initialDelayMs: number = 1000, operationName: string = 'operation', - fallbackFn?: () => Promise // Fallback (Mistral free model) ): Promise { let lastError: Error | undefined; @@ -67,26 +65,6 @@ async function retryWithBackoff( } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - // Check if we should fallback to Mistral model - if (fallbackFn && shouldFallback(error)) { - console.warn(`[Fallback] ${operationName} hit rate limit or error, trying Mistral fallback model...`); - currentModelType = 'fallback1'; - - // Try fallback with its own retry logic - try { - const result = await retryWithBackoff( - fallbackFn, - maxRetries, - initialDelayMs, - `${operationName} (fallback)` - ); - return result; - } catch (fallbackError) { - console.error(`[Fallback] Mistral model also failed:`, fallbackError instanceof Error ? fallbackError.message : fallbackError); - throw fallbackError; - } - } - if (attempt === maxRetries) { console.error(`[Retry] ${operationName} failed after ${maxRetries} attempts:`, lastError.message); throw lastError; @@ -94,7 +72,7 @@ async function retryWithBackoff( const delay = initialDelayMs * Math.pow(2, attempt - 1); console.warn(`[Retry] ${operationName} attempt ${attempt}/${maxRetries} failed: ${lastError.message}`); - console.warn(`[Retry] Waiting ${delay}ms before retry...`); + console.warn(`[Retry] Waiting ${delay}ms before retry (next call may route to a different free model)...`); await sleep(delay); } } @@ -258,10 +236,9 @@ async function analyzeModulesInParallel( riskPatterns: string ): Promise { console.log(`[MapReduce] Starting parallel analysis of ${chunks.length} modules...`); - console.log(`[MapReduce] Primary: ${MODEL_PRESETS.analyzer.model}, Fallback: ${MODEL_PRESETS.fallback.model}`); + console.log(`[MapReduce] Model: ${MODEL_PRESETS.analyzer.model}`); - const analyzerChain = await createAnalyzerChain(0); - const fallbackChain = await createAnalyzerChain(1); + const analyzerChain = await createAnalyzerChain(); // Create analysis tasks for each module const analysisPromises = chunks.map(async (chunk, index) => { @@ -282,13 +259,12 @@ async function analyzeModulesInParallel( 3, 2000, `Analyzer for ${chunk.moduleName}`, - () => fallbackChain.invoke(moduleInput) // Fallback (Mistral) ); const findings = await safeParseAnalyzerResponse(rawResponse); const duration = Date.now() - startTime; - console.log(`[Chunk ${index + 1}/${chunks.length}] ${chunk.moduleName}: ${findings.technical_findings?.length || 0} findings (${duration}ms, ${currentModelType} model)`); + console.log(`[Chunk ${index + 1}/${chunks.length}] ${chunk.moduleName}: ${findings.technical_findings?.length || 0} findings (${duration}ms)`); return { moduleName: chunk.moduleName, @@ -532,11 +508,8 @@ async function safeParseAnalyzerResponse(text: string): Promise { } // Agent 2: Scorer Chain -// fallbackLevel: 0 = primary (Xiaomi), 1 = fallback (Mistral) -async function createScorerChain(fallbackLevel: 0 | 1 = 0) { - const config = fallbackLevel === 1 - ? getFallbackConfig('scorer') - : getModelConfig('scorer'); +async function createScorerChain() { + const config = getModelConfig('scorer'); const llm = createLLM(config); const stringParser = new StringOutputParser(); @@ -730,11 +700,8 @@ function getDefaultReporterResponse(): ReporterResponse { } // Agent 3: Reporter Chain -// fallbackLevel: 0 = primary (Xiaomi), 1 = fallback (Mistral) -async function createReporterChain(fallbackLevel: 0 | 1 = 0) { - const config = fallbackLevel === 1 - ? getFallbackConfig('reporter') - : getModelConfig('reporter'); +async function createReporterChain() { + const config = getModelConfig('reporter'); const llm = createLLM(config); const stringParser = new StringOutputParser(); @@ -770,19 +737,14 @@ function getRiskLevel(score: number): 'low' | 'moderate' | 'high' | 'critical' { * - Multiple modules: Uses parallel chunked analysis (Map-Reduce pattern) */ export async function runLangChainAnalysis(input: ContractAnalysisInput): Promise { - const primaryModel = MODEL_PRESETS.analyzer.model; - const fallbackModel = MODEL_PRESETS.fallback.model; - - // Reset model tracking for this analysis run - currentModelType = 'primary'; + const model = MODEL_PRESETS.analyzer.model; // Initialize metrics collector for confidence scoring const metrics = createMetricsCollector(); try { console.log('[LangChain] Starting 3-agent analysis...'); - console.log(`[LangChain] Primary model: ${primaryModel}`); - console.log(`[LangChain] Fallback1: ${MODEL_PRESETS.fallback.model}, Fallback2: ${fallbackModel}`); + console.log(`[LangChain] Model: ${model} (auto-routes to available free models)`); // Update metrics from input data updateFromBytecode(metrics, input.disassembledCode, input.publicFunctions); @@ -808,19 +770,17 @@ export async function runLangChainAnalysis(input: ContractAnalysisInput): Promis } else { // ===== SINGLE-PASS: Traditional analysis ===== console.log(`[LangChain] Small contract: ${chunks.length} module(s), using single-pass analysis`); - console.log(`[Agent 1] Running Analyzer (${primaryModel})...`); + console.log(`[Agent 1] Running Analyzer (${model})...`); - const analyzerChain = await createAnalyzerChain(0); - const fallbackAnalyzerChain = await createAnalyzerChain(1); + const analyzerChain = await createAnalyzerChain(); const rawAnalyzerResponse = await retryWithBackoff( () => analyzerChain.invoke(input), 3, 2000, 'Analyzer LLM call', - () => fallbackAnalyzerChain.invoke(input) // Fallback (Mistral) ); - console.log(`[Agent 1] Raw response received (${currentModelType} model), parsing...`); + console.log('[Agent 1] Raw response received, parsing...'); findings = await safeParseAnalyzerResponse(rawAnalyzerResponse); } @@ -889,37 +849,33 @@ export async function runLangChainAnalysis(input: ContractAnalysisInput): Promis }; } - // Agent 2: Score with retry and fallback - console.log(`[Agent 2] Running Scorer (${primaryModel})...`); - const scorerChain = await createScorerChain(0); - const fallbackScorerChain = await createScorerChain(1); + // Agent 2: Score with retry + console.log(`[Agent 2] Running Scorer (${model})...`); + const scorerChain = await createScorerChain(); const rawScorerResponse = await retryWithBackoff( () => scorerChain.invoke({ findings }), 3, 2000, 'Scorer LLM call', - () => fallbackScorerChain.invoke({ findings }) // Fallback (Mistral) ); - console.log(`[Agent 2] Raw response received (${currentModelType} model), parsing...`); + console.log('[Agent 2] Raw response received, parsing...'); const score = await safeParseScorerResponse(rawScorerResponse); console.log(`[Agent 2] Risk score: ${score.risk_score}`); - // Agent 3: Report with retry and fallback - console.log(`[Agent 3] Running Reporter (${primaryModel})...`); - const reporterChain = await createReporterChain(0); - const fallbackReporterChain = await createReporterChain(1); + // Agent 3: Report with retry + console.log(`[Agent 3] Running Reporter (${model})...`); + const reporterChain = await createReporterChain(); const rawReporterResponse = await retryWithBackoff( () => reporterChain.invoke({ findings, score }), 3, 2000, 'Reporter LLM call', - () => fallbackReporterChain.invoke({ findings, score }) // Fallback (Mistral) ); - console.log(`[Agent 3] Raw response received (${currentModelType} model), parsing...`); + console.log('[Agent 3] Raw response received, parsing...'); const report = await safeParseReporterResponse(rawReporterResponse); console.log('[Agent 3] Report generated'); diff --git a/backend/src/lib/langchain-llm.ts b/backend/src/lib/langchain-llm.ts index 0f98264..572f2a9 100644 --- a/backend/src/lib/langchain-llm.ts +++ b/backend/src/lib/langchain-llm.ts @@ -4,30 +4,8 @@ export interface LLMConfig { model: string; temperature?: number; maxTokens?: number; - providerOrder?: string[]; // Provider order preference (e.g., ["deepinfra"]) - quantizations?: string[]; // Quantization filter (e.g., ["fp4"]) } -// Error types that should trigger fallback to paid model -const FALLBACK_ERROR_CODES = [ - 429, // Rate limit - 503, // Service unavailable - 502, // Bad gateway - 504, // Gateway timeout - 500, // Internal server error -]; - -const FALLBACK_ERROR_MESSAGES = [ - 'rate limit', - 'rate_limit', - 'too many requests', - 'timeout', - 'timed out', - 'service unavailable', - 'model is overloaded', - 'capacity', -]; - // Create LLM using OpenRouter export function createLLM(config: LLMConfig): ChatOpenAI { const apiKey = process.env.OPEN_ROUTER_KEY; @@ -41,31 +19,11 @@ export function createLLM(config: LLMConfig): ChatOpenAI { 'X-Title': 'RedFlag Smart Contract Analyzer', }; - // Build provider object for OpenRouter routing preferences - // OpenRouter expects this in the request body, not headers - const providerConfig: Record = {}; - - if (config.providerOrder && config.providerOrder.length > 0) { - providerConfig.order = config.providerOrder; - console.log(`[LLM] Provider order: ${config.providerOrder.join(', ')}`); - } - - if (config.quantizations && config.quantizations.length > 0) { - providerConfig.quantizations = config.quantizations; - console.log(`[LLM] Quantizations: ${config.quantizations.join(', ')}`); - } - - // Build modelKwargs only if we have provider config - const modelKwargs = Object.keys(providerConfig).length > 0 - ? { provider: providerConfig } - : undefined; - return new ChatOpenAI({ modelName: config.model, openAIApiKey: apiKey, temperature: config.temperature ?? 0.7, maxTokens: config.maxTokens ?? 4000, - modelKwargs, configuration: { baseURL: 'https://openrouter.ai/api/v1', apiKey: apiKey, @@ -75,37 +33,23 @@ export function createLLM(config: LLMConfig): ChatOpenAI { } // Model presets for each agent -// PRIMARY: Xiaomi free model (xiaomi/mimo-v2-flash:free) - reliable JSON output -// FALLBACK: Mistral free model (mistralai/devstral-2512:free) - coding-focused +// Uses openrouter/free — OpenRouter's smart router that selects from all available free models. +// Each retry may hit a different model, providing natural fallback behavior. export const MODEL_PRESETS = { analyzer: { - // Primary: Xiaomi free model (reliable JSON output) - model: 'xiaomi/mimo-v2-flash:free', + model: 'openrouter/free', temperature: 0.3, // Lower for technical analysis maxTokens: 6000, - providerOrder: ['xiaomi'], - quantizations: ['fp8'], }, scorer: { - model: 'xiaomi/mimo-v2-flash:free', + model: 'openrouter/free', temperature: 0.2, // Very low for consistent scoring maxTokens: 2000, - providerOrder: ['xiaomi'], - quantizations: ['fp8'], }, reporter: { - model: 'xiaomi/mimo-v2-flash:free', + model: 'openrouter/free', temperature: 0.7, // Higher for creative writing maxTokens: 4000, - providerOrder: ['xiaomi'], - quantizations: ['fp8'], - }, - // Fallback: Mistral's free coding model - fallback: { - model: 'mistralai/devstral-2512:free', - temperature: 0.5, - maxTokens: 6000, - providerOrder: ['mistral'], }, } as const; @@ -117,48 +61,41 @@ export function getModelConfig(agentName: keyof typeof MODEL_PRESETS): LLMConfig return { model: envModel || preset.model, temperature: preset.temperature, - maxTokens: 'maxTokens' in preset ? preset.maxTokens : undefined, - providerOrder: 'providerOrder' in preset ? [...preset.providerOrder] : undefined, - quantizations: 'quantizations' in preset ? [...preset.quantizations] : undefined, + maxTokens: preset.maxTokens, }; } -// Get fallback model config (Mistral free model) -export function getFallbackConfig(agentName: keyof typeof MODEL_PRESETS): LLMConfig { - const fallback = MODEL_PRESETS.fallback; - const primary = MODEL_PRESETS[agentName]; - - return { - model: fallback.model, - temperature: primary.temperature, // Use agent's preferred temperature - maxTokens: fallback.maxTokens, - providerOrder: [...fallback.providerOrder], - }; -} - -// Check if error should trigger fallback to paid model -export function shouldFallback(error: unknown): boolean { +// Check if error should trigger a retry (rate limit, service unavailable, etc.) +export function shouldRetry(error: unknown): boolean { if (!error) return false; + const RETRY_ERROR_CODES = [429, 503, 502, 504, 500]; + const RETRY_ERROR_MESSAGES = [ + 'rate limit', + 'rate_limit', + 'too many requests', + 'timeout', + 'timed out', + 'service unavailable', + 'model is overloaded', + 'capacity', + ]; + const errorObj = error as Record; // Check HTTP status code - if (typeof errorObj.status === 'number' && FALLBACK_ERROR_CODES.includes(errorObj.status)) { + if (typeof errorObj.status === 'number' && RETRY_ERROR_CODES.includes(errorObj.status)) { return true; } // Check error message const message = (errorObj.message || errorObj.error || String(error)).toString().toLowerCase(); - return FALLBACK_ERROR_MESSAGES.some(pattern => message.includes(pattern)); + return RETRY_ERROR_MESSAGES.some(pattern => message.includes(pattern)); } -// Create LLM with fallback capability -export function createLLMWithFallback( - primaryConfig: LLMConfig, - fallbackConfig: LLMConfig -): { primary: ChatOpenAI; fallback: ChatOpenAI } { - return { - primary: createLLM(primaryConfig), - fallback: createLLM(fallbackConfig), - }; +// Validate that required API keys are set +export function validateApiKeys() { + if (!process.env.OPEN_ROUTER_KEY) { + throw new Error('[LLM] OPEN_ROUTER_KEY is required. Set it in your .env file.'); + } } diff --git a/backend/src/lib/llm-analyzer.ts b/backend/src/lib/llm-analyzer.ts index b284d85..e0c547a 100644 --- a/backend/src/lib/llm-analyzer.ts +++ b/backend/src/lib/llm-analyzer.ts @@ -380,13 +380,13 @@ export async function runFullAnalysisChain(packageId: string, network: string, s // RUN LANGCHAIN 3-AGENT ANALYSIS // ============================================================ console.log('[4/6] Running LangChain 3-agent analysis...'); - console.log('[4/6] Using: nvidia/nemotron-3-nano-30b-a3b:free via Nvidia'); + console.log('[4/6] Using: openrouter/free (auto-selects from available free models)'); // Record module and function stats for audit const moduleCount = Object.keys(disassembledCode).length; audit.recordModuleStats(moduleCount, moduleCount); audit.recordFunctionStats(publicFunctions.length, publicFunctions.length); - audit.recordModel('nvidia/nemotron-3-nano-30b-a3b:free'); + audit.recordModel('openrouter/free'); try { const safetyCard = await runLangChainAnalysisWithFallback({