UNPKG

claude-gemini-multimodal-bridge

Version:

Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities

447 lines (446 loc) 19 kB
import { commandExists } from '../utils/platformUtils.js'; import { execSync } from 'child_process'; import { logger } from '../utils/logger.js'; import { safeExecute } from '../utils/errorHandler.js'; import { OAuthManager } from './OAuthManager.js'; import { AuthCache } from './AuthCache.js'; export class AuthVerifier { oauthManager; authCache; constructor() { this.oauthManager = new OAuthManager(); this.authCache = AuthCache.getInstance(); setInterval(() => { this.authCache.cleanup(); }, 30 * 60 * 1000); } async verifyAllAuthentications() { return safeExecute(async () => { logger.info('Starting comprehensive authentication verification...'); const services = { gemini: await this.verifyGeminiAuth(), aistudio: await this.verifyAIStudioAuth(), claude: await this.verifyClaudeCodeAuth(), }; const overall = Object.values(services).every(result => result.success); const recommendations = this.generateRecommendations(services); logger.info('Authentication verification completed', { overall, servicesVerified: Object.keys(services).length, successCount: Object.values(services).filter(r => r.success).length, }); return { overall, services, recommendations, }; }, { operationName: 'verify-all-auth', layer: 'claude', timeout: 30000, }); } async verifyGeminiAuth() { const cachedResult = this.authCache.get('gemini'); if (cachedResult) { return cachedResult; } return safeExecute(async () => { logger.info('Verifying Gemini authentication (no cache)...'); try { const oauthStatus = await this.oauthManager.checkGeminiAuthentication(); if (oauthStatus.isAuthenticated) { const result = { success: true, status: oauthStatus, requiresAction: false, }; this.authCache.set('gemini', result); return result; } const apiKey = process.env.GEMINI_API_KEY; if (apiKey) { logger.debug('OAuth failed, trying API Key fallback for Gemini'); try { await this.testGeminiApiKey(apiKey); const result = { success: true, status: { isAuthenticated: true, method: 'api_key', userInfo: undefined, }, requiresAction: false, }; this.authCache.set('gemini', result); return result; } catch (apiError) { logger.warn('Gemini API key validation failed', { error: apiError.message }); } } const result = { success: false, status: { isAuthenticated: false, method: 'oauth', userInfo: undefined, }, error: 'Gemini not authenticated', requiresAction: true, actionInstructions: 'Run "gemini auth" for OAuth (recommended) or set GEMINI_API_KEY environment variable', }; this.authCache.set('gemini', result); const failureInfo = this.authCache.getFailureInfo('gemini'); if (failureInfo?.nextRetryTime) { result.error += ` (Failure #${failureInfo.count}, retry after ${failureInfo.nextRetryTime.toLocaleTimeString()})`; } return result; } catch (error) { logger.error('Gemini authentication verification failed', { error: error.message }); const result = { success: false, status: { isAuthenticated: false, method: 'oauth', userInfo: undefined, }, error: `Gemini verification failed: ${error.message}`, requiresAction: true, actionInstructions: 'Install Gemini CLI: npm install -g @google/gemini-cli && gemini auth', }; return result; } }, { operationName: 'verify-gemini-auth', layer: 'gemini', timeout: 10000, }); } async verifyAIStudioAuth() { const cachedResult = this.authCache.get('aistudio'); if (cachedResult) { return cachedResult; } return safeExecute(async () => { logger.info('Verifying AI Studio authentication (no cache)...'); const preferredKey = process.env.AI_STUDIO_API_KEY; const fallback1 = process.env.GOOGLE_AI_STUDIO_API_KEY; const fallback2 = process.env.GEMINI_API_KEY; const apiKey = preferredKey ?? fallback1 ?? fallback2; logger.debug('AI Studio authentication verification', { hasPreferredKey: !!preferredKey, hasFallback1: !!fallback1, hasFallback2: !!fallback2, selectedKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'none', searchOrder: ['AI_STUDIO_API_KEY', 'GOOGLE_AI_STUDIO_API_KEY', 'GEMINI_API_KEY'], errorReference: 'Addressing authentication issues from Error.md lines 70-86' }); if (!preferredKey && fallback2) { logger.warn('GEMINI_API_KEY is deprecated for AI Studio. Please use AI_STUDIO_API_KEY instead.', { currentVar: 'GEMINI_API_KEY', recommendedVar: 'AI_STUDIO_API_KEY', migration: 'Update your .env file: GEMINI_API_KEY → AI_STUDIO_API_KEY', reason: 'GEMINI_API_KEY is ambiguous - used for both Gemini CLI and AI Studio' }); } if (!preferredKey && fallback1) { logger.warn('GOOGLE_AI_STUDIO_API_KEY is deprecated. Please use AI_STUDIO_API_KEY instead.', { currentVar: 'GOOGLE_AI_STUDIO_API_KEY', recommendedVar: 'AI_STUDIO_API_KEY', migration: 'Update your .env file: GOOGLE_AI_STUDIO_API_KEY → AI_STUDIO_API_KEY' }); } if (!apiKey) { logger.error('AI Studio API key missing - this causes the authentication failure seen in Error.md', { issue: 'No API key found in any environment variable', searchedVars: ['AI_STUDIO_API_KEY', 'GOOGLE_AI_STUDIO_API_KEY', 'GEMINI_API_KEY'], errorContext: 'This is the root cause of AI Studio authentication failures', setupUrl: 'https://aistudio.google.com/app/apikey' }); const result = { success: false, status: { isAuthenticated: false, method: 'api_key', userInfo: undefined, }, error: 'AI Studio API key not found. This causes authentication failures as seen in Error.md.', requiresAction: true, actionInstructions: 'Set AI_STUDIO_API_KEY environment variable with your AI Studio API key. Get it from: https://aistudio.google.com/app/apikey', }; this.authCache.set('aistudio', result); const failureInfo = this.authCache.getFailureInfo('aistudio'); if (failureInfo?.nextRetryTime) { result.error += ` (Failure #${failureInfo.count}, retry after ${failureInfo.nextRetryTime.toLocaleTimeString()})`; } return result; } if (!this.validateAIStudioApiKeyFormat(apiKey)) { logger.error('Invalid AI Studio API key format detected', { keyPrefix: apiKey.substring(0, 8), keyLength: apiKey.length, expectedFormat: 'Should start with "AI" and be at least 20 characters', currentFormat: `Starts with "${apiKey.substring(0, 2)}", length: ${apiKey.length}`, troubleshooting: 'Verify the key was copied correctly from AI Studio' }); return { success: false, status: { isAuthenticated: false, method: 'api_key', userInfo: undefined, }, error: 'Invalid AI Studio API key format. Expected format: starts with "AI", minimum 20 characters.', requiresAction: true, actionInstructions: 'Verify your API key from https://aistudio.google.com/app/apikey and update AI_STUDIO_API_KEY in your .env file', }; } logger.info('AI Studio authentication verification successful', { method: 'api_key', keySource: preferredKey ? 'AI_STUDIO_API_KEY' : fallback1 ? 'GOOGLE_AI_STUDIO_API_KEY' : 'GEMINI_API_KEY', status: 'ready' }); const result = { success: true, status: { isAuthenticated: true, method: 'api_key', userInfo: { planType: 'free', }, }, requiresAction: false, }; this.authCache.set('aistudio', result); return result; }, { operationName: 'verify-aistudio-auth', layer: 'aistudio', timeout: 10000, }); } validateAIStudioApiKeyFormat(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return false; } return apiKey.length >= 20 && apiKey.startsWith('AI'); } async verifyClaudeCodeAuth() { const cachedResult = this.authCache.get('claude'); if (cachedResult) { return cachedResult; } return safeExecute(async () => { logger.info('Verifying Claude Code authentication...'); try { const isInstalled = await this.checkClaudeCodeInstalled(); if (!isInstalled) { const result = { success: false, status: { isAuthenticated: false, method: 'session', userInfo: undefined, }, error: 'Claude Code not installed', requiresAction: true, actionInstructions: 'Install Claude Code: npm install -g @anthropic-ai/claude-code', }; this.authCache.set('claude', result); return result; } const isWorking = await this.testClaudeCodeFunctionality(); if (!isWorking) { const result = { success: false, status: { isAuthenticated: false, method: 'session', userInfo: undefined, }, error: 'Claude Code authentication required', requiresAction: true, actionInstructions: 'Run "claude auth" to authenticate Claude Code', }; this.authCache.set('claude', result); return result; } const result = { success: true, status: { isAuthenticated: true, method: 'session', userInfo: { planType: 'authenticated', }, }, requiresAction: false, }; this.authCache.set('claude', result); return result; } catch (error) { logger.error('Claude Code verification failed', { error: error.message }); return { success: false, status: { isAuthenticated: false, method: 'session', userInfo: undefined, }, error: `Claude Code verification failed: ${error.message}`, requiresAction: true, actionInstructions: 'Ensure Claude Code is properly installed and authenticated', }; } }, { operationName: 'verify-claude-auth', layer: 'claude', timeout: 10000, }); } async verifyServiceAuth(service) { switch (service) { case 'gemini': return this.verifyGeminiAuth(); case 'aistudio': return this.verifyAIStudioAuth(); case 'claude': return this.verifyClaudeCodeAuth(); default: throw new Error(`Unknown service: ${service}`); } } async testGeminiApiKey(apiKey) { const { GoogleGenAI } = await import('@google/genai'); try { const genAI = new GoogleGenAI({ apiKey }); const result = await genAI.models.generateContent({ model: 'gemini-pro', contents: [{ parts: [{ text: 'Test' }] }] }); if (!result.candidates || result.candidates.length === 0) { throw new Error('API key test failed - no response'); } logger.debug('Gemini API key validation successful'); } catch (error) { const errorMessage = error.message; if (errorMessage.includes('429') || errorMessage.includes('quota') || errorMessage.includes('Quota exceeded')) { logger.warn('Gemini quota exceeded during authentication check', { error: errorMessage }); throw new Error(`Gemini API quota exceeded. Please wait before retrying or use a different model. Details: ${errorMessage}`); } logger.warn('Gemini API key validation failed', { error: errorMessage }); throw new Error(`Gemini API key invalid: ${errorMessage}`); } } getAuthCacheStats() { return this.authCache.getStats(); } clearAuthCache(service) { if (service) { this.authCache.invalidate(service); logger.info('Authentication cache cleared for service', { service }); } else { this.authCache.clear(); logger.info('All authentication cache cleared'); } } async forceRefreshAuth(service) { this.authCache.forceRefresh(service); return await this.verifyServiceAuth(service); } async checkClaudeCodeInstalled() { if (commandExists('claude')) { return true; } try { execSync('claude --version', { stdio: 'ignore', timeout: 5000 }); return true; } catch { return false; } } async testClaudeCodeFunctionality() { try { const output = execSync('claude --help', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }); const needsAuth = output.toLowerCase().includes('auth') && (output.toLowerCase().includes('required') || output.toLowerCase().includes('login')); return !needsAuth; } catch (error) { logger.debug('Claude Code functionality test failed', { error: error.message }); return false; } } generateRecommendations(services) { const recommendations = []; if (services.gemini && !services.gemini.success) { if (services.gemini.actionInstructions) { recommendations.push(`Gemini: ${services.gemini.actionInstructions}`); } } if (services.aistudio && !services.aistudio.success) { if (services.aistudio.actionInstructions) { recommendations.push(`AI Studio: ${services.aistudio.actionInstructions}`); } } if (services.claude && !services.claude.success) { if (services.claude.actionInstructions) { recommendations.push(`Claude Code: ${services.claude.actionInstructions}`); } } if (recommendations.length === 0) { recommendations.push('All services are properly authenticated and ready to use!'); } else { recommendations.push('For setup assistance, run: cgmb setup-guide'); } return recommendations; } async hasAnyAuthentication() { try { const results = await this.verifyAllAuthentications(); return Object.values(results.services).some(service => service.success); } catch { return false; } } async getServiceStatus(service) { try { const result = await this.verifyServiceAuth(service); if (result.success) { const method = result.status.method; const user = result.status.userInfo?.email ?? 'authenticated user'; return `✅ ${service}: Authenticated via ${method} (${user})`; } else { return `❌ ${service}: ${result.error}`; } } catch (error) { return `❌ ${service}: Verification failed - ${error.message}`; } } async serviceNeedsAttention(service) { try { const result = await this.verifyServiceAuth(service); return !result.success && result.requiresAction; } catch { return true; } } }