diff --git a/.env.example b/.env.example index c5de778..00d8892 100644 --- a/.env.example +++ b/.env.example @@ -100,6 +100,10 @@ PAYPAL_CLIENT_SECRET=your_paypal_client_secret_here # AI & ML SERVICES # ============================================================================ +# Multi-Model Toolkit Configuration +# Set to FALSE to use single-model mode (works with just one API key) +USE_MULTI_MODEL=TRUE + # OpenAI - GPT models and embeddings # Get from: https://platform.openai.com/api-keys OPENAI_API_KEY=sk-your_openai_api_key_here @@ -109,6 +113,10 @@ OPENAI_ORG_ID=org-your_org_id_here # Get from: https://console.anthropic.com/settings/keys ANTHROPIC_API_KEY=sk-ant-your_anthropic_api_key_here +# Google Gemini - Gemini models +# Get from: https://aistudio.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here + # ============================================================================ # CONTENT & MEDIA # ============================================================================ diff --git a/tools/multi-model/README.md b/tools/multi-model/README.md index 5904c4b..4acbf9c 100644 --- a/tools/multi-model/README.md +++ b/tools/multi-model/README.md @@ -16,6 +16,7 @@ keywords - **šŸ’° Cost Optimization** - Use cheap models for simple tasks, reserve power for complex ones +- **šŸ”§ Flexible Configuration** - Works with just one API key or all three ## File Structure @@ -51,6 +52,49 @@ ANTHROPIC_API_KEY=sk-ant-... GEMINI_API_KEY=... ``` +## Configuration + +### Single-Model vs Multi-Model Mode + +The toolkit automatically adapts based on your available API keys: + +| API Keys Available | Mode | Behavior | +| ------------------ | ------------ | ---------------------------------- | +| All three | Multi-model | Consensus review across all models | +| Two keys | Multi-model | Consensus with 2 models | +| One key | Single-model | Direct review with one model | + +**To explicitly disable multi-model mode** (even if you have multiple keys): + +```bash +# In your .env file +USE_MULTI_MODEL=FALSE +``` + +This is useful for: + +- Reducing API costs +- Faster reviews (single API call) +- Testing with a specific model + +### Minimum Requirements + +You only need **one API key** to use this toolkit: + +```bash +# Option 1: OpenAI only +OPENAI_API_KEY=sk-... + +# Option 2: Anthropic/Claude only +ANTHROPIC_API_KEY=sk-ant-... + +# Option 3: Gemini only +GEMINI_API_KEY=... +``` + +The toolkit will automatically use the best available model for your +configuration. + ## CLI Tools ### Multi-Model Code Review diff --git a/tools/multi-model/index.js b/tools/multi-model/index.js index 2295371..9aa07ea 100644 --- a/tools/multi-model/index.js +++ b/tools/multi-model/index.js @@ -6,10 +6,12 @@ * - Anthropic (Claude 3 Opus, Sonnet, Haiku) * - Google (Gemini Pro, Flash) * + * Supports single-model mode when USE_MULTI_MODEL=FALSE or only one API key is available. + * * @example * import { reviewCode, createRouter, searchCodebase } from '@claude-sidekick/multi-model'; * - * // Multi-model code review + * // Multi-model code review (auto-adapts to available API keys) * const results = await reviewCode(code, { filename: 'app.js' }); * * // Intelligent routing @@ -20,7 +22,7 @@ * const matches = await searchCodebase('authentication logic'); */ -// Clients +// Clients & Configuration export { complete, embed, @@ -30,10 +32,24 @@ export { getGemini, MODELS, MODEL_COSTS, + // Configuration helpers + isMultiModelEnabled, + getAvailableProviders, + getAvailableModels, + getDefaultModel, + getBestModel, + hasProvider, + filterAvailableModels, } from './lib/clients.js'; // Code Review -export { reviewCode, quickReview, deepReview, formatReviewResults } from './lib/code-review.js'; +export { + reviewCode, + quickReview, + deepReview, + singleReview, + formatReviewResults, +} from './lib/code-review.js'; // Model Router export { diff --git a/tools/multi-model/lib/clients.js b/tools/multi-model/lib/clients.js index cfd74c8..a8ce2d0 100644 --- a/tools/multi-model/lib/clients.js +++ b/tools/multi-model/lib/clients.js @@ -1,6 +1,8 @@ /** * Multi-Model AI Clients * Unified interface for OpenAI, Anthropic, and Google Gemini + * + * Supports single-model mode when USE_MULTI_MODEL=FALSE or only one API key is available */ import OpenAI from 'openai'; @@ -15,6 +17,46 @@ let openaiClient = null; let anthropicClient = null; let geminiClient = null; +/** + * Check if multi-model mode is enabled + */ +export function isMultiModelEnabled() { + const setting = process.env.USE_MULTI_MODEL?.toUpperCase(); + // Default to TRUE if not set, FALSE only if explicitly disabled + return setting !== 'FALSE' && setting !== '0' && setting !== 'NO'; +} + +/** + * Check which API keys are available + */ +export function getAvailableProviders() { + const providers = []; + if (process.env.OPENAI_API_KEY) providers.push('openai'); + if (process.env.ANTHROPIC_API_KEY) providers.push('anthropic'); + if (process.env.GEMINI_API_KEY || process.env.GEMENI_API_KEY) providers.push('gemini'); + return providers; +} + +/** + * Check if a specific provider is available + */ +export function hasProvider(provider) { + return getAvailableProviders().includes(provider); +} + +/** + * Get the primary (first available) provider + */ +export function getPrimaryProvider() { + const providers = getAvailableProviders(); + if (providers.length === 0) { + throw new Error( + 'No API keys configured. Set at least one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY' + ); + } + return providers[0]; +} + export function getOpenAI() { if (!openaiClient) { if (!process.env.OPENAI_API_KEY) { @@ -158,3 +200,61 @@ export const MODEL_COSTS = { 'gemini-1.5-pro': [3.5, 10.5], 'gemini-2.0-flash': [0.075, 0.3], }; + +/** + * Get provider from model name + */ +export function getProviderFromModel(model) { + if (model.startsWith('gpt-')) return 'openai'; + if (model.startsWith('claude-')) return 'anthropic'; + if (model.startsWith('gemini-')) return 'gemini'; + return null; +} + +/** + * Get available models based on configured API keys + */ +export function getAvailableModels() { + const models = []; + if (hasProvider('openai')) { + models.push(MODELS.GPT4O, MODELS.GPT4O_MINI); + } + if (hasProvider('anthropic')) { + models.push(MODELS.CLAUDE_SONNET, MODELS.CLAUDE_HAIKU); + } + if (hasProvider('gemini')) { + models.push(MODELS.GEMINI_PRO, MODELS.GEMINI_FLASH); + } + return models; +} + +/** + * Get the default model (cheapest available fast model) + */ +export function getDefaultModel() { + // Prefer cheaper/faster models as default + if (hasProvider('gemini')) return MODELS.GEMINI_FLASH; + if (hasProvider('openai')) return MODELS.GPT4O_MINI; + if (hasProvider('anthropic')) return MODELS.CLAUDE_HAIKU; + throw new Error('No API keys configured'); +} + +/** + * Get the best available model for quality + */ +export function getBestModel() { + if (hasProvider('anthropic')) return MODELS.CLAUDE_SONNET; + if (hasProvider('openai')) return MODELS.GPT4O; + if (hasProvider('gemini')) return MODELS.GEMINI_PRO; + throw new Error('No API keys configured'); +} + +/** + * Filter models to only those with available API keys + */ +export function filterAvailableModels(models) { + return models.filter((model) => { + const provider = getProviderFromModel(model); + return provider && hasProvider(provider); + }); +} diff --git a/tools/multi-model/lib/code-review.js b/tools/multi-model/lib/code-review.js index f002942..c947946 100644 --- a/tools/multi-model/lib/code-review.js +++ b/tools/multi-model/lib/code-review.js @@ -1,9 +1,19 @@ /** * Multi-Model Code Review System * Run code through multiple AI models and find consensus issues + * + * Supports single-model mode when USE_MULTI_MODEL=FALSE or only one API key is available */ -import { complete, MODELS } from './clients.js'; +import { + complete, + MODELS, + isMultiModelEnabled, + getAvailableProviders, + filterAvailableModels, + getDefaultModel, + getBestModel, +} from './clients.js'; import chalk from 'chalk'; const REVIEW_PROMPT = `You are an expert code reviewer. Analyze the following code and identify issues. @@ -105,15 +115,63 @@ function findConsensus(reviews) { /** * Multi-model code review with consensus + * Falls back to single-model mode when USE_MULTI_MODEL=FALSE or only one API key is available */ export async function reviewCode(code, options = {}) { const { filename = 'code', - models = [MODELS.GEMINI_FLASH, MODELS.GPT4O_MINI, MODELS.CLAUDE_HAIKU], + models: requestedModels = [MODELS.GEMINI_FLASH, MODELS.GPT4O_MINI, MODELS.CLAUDE_HAIKU], consensusThreshold = 2, // How many models must agree verbose = false, } = options; + // Check if multi-model is enabled and what providers are available + const multiModelEnabled = isMultiModelEnabled(); + const availableProviders = getAvailableProviders(); + + // Filter to only available models + let models = filterAvailableModels(requestedModels); + + // Determine mode + const useSingleModel = !multiModelEnabled || availableProviders.length === 1 || models.length < 2; + + if (useSingleModel) { + // Single model mode - use the best available model + const singleModel = models.length > 0 ? models[0] : getBestModel(); + console.log(chalk.blue(`\nšŸ” Reviewing ${filename} with ${singleModel} (single-model mode)\n`)); + + if (!multiModelEnabled) { + console.log(chalk.dim('Multi-model mode disabled via USE_MULTI_MODEL=FALSE')); + } else if (availableProviders.length === 1) { + console.log(chalk.dim(`Only ${availableProviders[0]} API key configured`)); + } + + const startTime = Date.now(); + const review = await reviewWithModel(code, singleModel, filename); + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + + // In single model mode, all issues are "confirmed" (no consensus possible) + return { + filename, + models: [singleModel], + reviews: [review], + confirmedIssues: review.issues.map((issue) => ({ + ...issue, + models: [singleModel], + count: 1, + })), + possibleIssues: [], + elapsed, + singleModelMode: true, + summary: generateSingleModelSummary(review), + }; + } + + // Multi-model mode + if (models.length === 0) { + throw new Error('No models available. Check your API keys.'); + } + console.log(chalk.blue(`\nšŸ” Reviewing ${filename} with ${models.length} models...\n`)); // Run reviews in parallel @@ -142,10 +200,29 @@ export async function reviewCode(code, options = {}) { confirmedIssues, possibleIssues, elapsed, + singleModelMode: false, summary: generateSummary(confirmedIssues, possibleIssues), }; } +/** + * Generate summary for single-model review + */ +function generateSingleModelSummary(review) { + if (review.error) { + return `āŒ Review failed: ${review.summary}`; + } + if (review.issues.length === 0) { + return 'āœ… No issues found'; + } + const critical = review.issues.filter((i) => i.severity === 'critical').length; + const high = review.issues.filter((i) => i.severity === 'high').length; + const parts = [`āš ļø ${review.issues.length} issue(s) found`]; + if (critical > 0) parts.push(`🚨 ${critical} critical`); + if (high > 0) parts.push(`šŸ”“ ${high} high`); + return parts.join(' | '); +} + /** * Generate a human-readable summary */ @@ -174,12 +251,21 @@ function generateSummary(confirmed, possible) { export function formatReviewResults(results) { const lines = []; lines.push(chalk.bold(`\nšŸ“‹ Code Review Results: ${results.filename}`)); - lines.push(chalk.dim(`Models: ${results.models.join(', ')}`)); + + if (results.singleModelMode) { + lines.push(chalk.dim(`Model: ${results.models[0]} (single-model mode)`)); + } else { + lines.push(chalk.dim(`Models: ${results.models.join(', ')}`)); + } lines.push(chalk.dim(`Time: ${results.elapsed}s\n`)); lines.push(results.summary); if (results.confirmedIssues.length > 0) { - lines.push(chalk.bold('\nšŸ”“ Confirmed Issues (multiple models agree):')); + const headerText = results.singleModelMode + ? '\nšŸ” Issues Found:' + : '\nšŸ”“ Confirmed Issues (multiple models agree):'; + lines.push(chalk.bold(headerText)); + for (const issue of results.confirmedIssues) { const severityColor = { critical: 'red', high: 'yellow', medium: 'cyan', low: 'dim' }[issue.severity] || 'white'; @@ -190,7 +276,11 @@ export function formatReviewResults(results) { ); lines.push(` ${issue.description}`); lines.push(chalk.green(` → ${issue.suggestion}`)); - lines.push(chalk.dim(` Flagged by: ${issue.models.join(', ')}\n`)); + if (!results.singleModelMode) { + lines.push(chalk.dim(` Flagged by: ${issue.models.join(', ')}\n`)); + } else { + lines.push(''); // blank line + } } } @@ -207,17 +297,19 @@ export function formatReviewResults(results) { /** * Quick review with fastest/cheapest models + * Automatically adapts to available API keys */ export async function quickReview(code, filename = 'code') { return reviewCode(code, { filename, - models: [MODELS.GEMINI_FLASH, MODELS.GPT4O_MINI], + models: [MODELS.GEMINI_FLASH, MODELS.GPT4O_MINI, MODELS.CLAUDE_HAIKU], consensusThreshold: 2, }); } /** * Deep review with most capable models + * Automatically adapts to available API keys */ export async function deepReview(code, filename = 'code') { return reviewCode(code, { @@ -227,3 +319,16 @@ export async function deepReview(code, filename = 'code') { verbose: true, }); } + +/** + * Single model review (explicit single-model mode) + * Uses the best available model + */ +export async function singleReview(code, filename = 'code', model = null) { + const useModel = model || getBestModel(); + return reviewCode(code, { + filename, + models: [useModel], + consensusThreshold: 1, + }); +}