UNPKG

multi-llm-api-gateway

Version:

A comprehensive API gateway that enables Claude Code to work with 36+ LLM providers including OpenAI, Google Gemini, Anthropic, Ollama, and more

580 lines (498 loc) β€’ 17 kB
const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const { LLMInterface } = require('llm-interface'); const { v4: uuidv4 } = require('uuid'); require('dotenv').config(); const DynamicConfigManager = require('./config/dynamic-config-manager'); const ClaudeCompatibility = require('./claude-compatibility'); const ProviderRouter = require('./provider-router'); class ClaudeLLMGateway { constructor() { this.app = express(); this.configManager = new DynamicConfigManager(); this.claudeCompat = new ClaudeCompatibility(); this.providerRouter = new ProviderRouter(); this.providers = new Map(); this.requestLog = new Map(); } /** * Initialize gateway */ async initialize() { console.log('πŸš€ Initializing Claude LLM Gateway...'); try { // 1. Setup dynamic providers await this.setupDynamicProviders(); // 2. Setup middleware this.setupMiddleware(); // 3. Setup routes this.setupRoutes(); // 4. Error handling this.setupErrorHandling(); console.log('βœ… Gateway initialization completed'); } catch (error) { console.error('❌ Gateway initialization failed:', error); throw error; } } /** * Setup dynamic providers */ async setupDynamicProviders() { try { console.log('πŸ” 正在Setup dynamic providers...'); // Check if configuration needs updating const shouldUpdate = await this.configManager.shouldUpdateConfig(); if (shouldUpdate) { console.log('πŸ“ Updating provider configuration...'); await this.configManager.discoverProviders(); } // Load configuration const config = await this.configManager.loadConfig(); if (!config) { throw new Error('Unable to load provider configuration'); } // Set API keys const apiKeys = this.extractApiKeys(config.providers); LLMInterface.setApiKey(apiKeys); // Initialize provider router await this.providerRouter.initialize(config.providers); // Store provider configuration this.providers = new Map(Object.entries(config.providers)); console.log(`βœ… Successfully configured ${this.providers.size} providers`); // Show configuration summary this.displayProviderSummary(config.providers); } catch (error) { console.error('❌ Dynamic provider configuration failed:', error); throw error; } } /** * Extract API keys */ extractApiKeys(providers) { const apiKeys = {}; for (const [name, config] of Object.entries(providers)) { if (config.enabled && config.requires_api_key) { const envVar = this.getApiKeyEnvVar(name); if (process.env[envVar]) { apiKeys[name] = process.env[envVar]; } } else if (!config.requires_api_key) { // For providers that don't require API keys (like Ollama) apiKeys[name] = 'local'; } } return apiKeys; } /** * Get API key environment variable name */ getApiKeyEnvVar(providerName) { const envVars = { 'openai': 'OPENAI_API_KEY', 'anthropic': 'ANTHROPIC_API_KEY', 'google': 'GOOGLE_API_KEY', 'cohere': 'COHERE_API_KEY', 'huggingface': 'HUGGINGFACE_API_KEY', 'mistral': 'MISTRAL_API_KEY', 'groq': 'GROQ_API_KEY', 'perplexity': 'PERPLEXITY_API_KEY', 'ai21': 'AI21_API_KEY', 'nvidia': 'NVIDIA_API_KEY', 'fireworks': 'FIREWORKS_API_KEY', 'together': 'TOGETHER_API_KEY', 'replicate': 'REPLICATE_API_KEY' }; return envVars[providerName] || `${providerName.toUpperCase()}_API_KEY`; } /** * Show provider configuration summary */ displayProviderSummary(providers) { const enabled = Object.entries(providers).filter(([name, conf]) => conf.enabled); const local = enabled.filter(([name, conf]) => conf.local); const remote = enabled.filter(([name, conf]) => !conf.local); console.log('\nπŸ“Š Provider Configuration Summary:'); console.log(`πŸ”— Remote providers (${remote.length}): ${remote.map(([name]) => name).join(', ')}`); console.log(`🏠 Local providers (${local.length}): ${local.map(([name]) => name).join(', ')}`); console.log(`πŸ’° Total ${enabled.reduce((sum, [name, conf]) => sum + (conf.models?.length || 0), 0)} available models`); } /** * Setup middleware */ setupMiddleware() { // Security middleware this.app.use(helmet()); // CORS this.app.use(cors({ origin: process.env.CORS_ORIGIN || '*', methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key'] })); // Request parsing this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); // Rate limiting const limiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000, // 1 minute max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, // 100 requests per minute message: { error: 'Too many requests, please try again later.' }, standardHeaders: true, legacyHeaders: false }); this.app.use(limiter); // Request logging this.app.use((req, res, next) => { const requestId = uuidv4(); req.requestId = requestId; req.startTime = Date.now(); console.log(`πŸ“¨ ${req.method} ${req.path} [${requestId}]`); next(); }); } /** * Setup routes */ setupRoutes() { // Claude Code compatible API endpoints this.app.post('/v1/messages', this.handleClaudeMessages.bind(this)); this.app.post('/v1/chat/completions', this.handleClaudeChat.bind(this)); this.app.post('/anthropic/v1/messages', this.handleClaudeMessages.bind(this)); // Management endpoints this.app.get('/health', this.handleHealth.bind(this)); this.app.get('/providers', this.handleProviders.bind(this)); this.app.get('/providers/refresh', this.handleRefreshProviders.bind(this)); this.app.get('/models', this.handleModels.bind(this)); this.app.get('/config', this.handleConfig.bind(this)); this.app.get('/stats', this.handleStats.bind(this)); // Root path this.app.get('/', this.handleRoot.bind(this)); } /** * Handle Claude message requests */ async handleClaudeMessages(req, res) { const requestId = req.requestId; try { console.log(`πŸ€– Handle Claude message requests [${requestId}]`); // Validate request format const validationErrors = this.claudeCompat.validateClaudeRequest(req.body); if (validationErrors.length > 0) { return res.status(400).json({ error: { type: 'invalid_request_error', message: validationErrors.join('; ') } }); } // Select provider const provider = await this.providerRouter.selectProvider(req.body); console.log(`🎯 Selected provider: ${provider} [${requestId}]`); // Record request this.providerRouter.recordRequest(provider); // Transform request format const llmRequest = this.claudeCompat.toLLMInterface(req.body, provider); // Call llm-interface console.log(`πŸš€ Sending request to ${provider} [${requestId}]`); const startTime = Date.now(); let response; if (req.body.stream) { // Handle streaming response response = await this.handleStreamRequest(llmRequest, provider, res, requestId); return; // Streaming response returns directly } else { // Handle normal response response = await LLMInterface.sendMessage(provider, llmRequest); } const processingTime = Date.now() - startTime; console.log(`βœ… Request completed ${provider} (${processingTime}ms) [${requestId}]`); // Transform back to Claude format const claudeResponse = this.claudeCompat.toClaudeFormat(response, provider, requestId); // Record response time this.logRequest(requestId, provider, processingTime, true); res.json(claudeResponse); } catch (error) { console.error(`❌ Request processing failed [${requestId}]:`, error); this.handleRequestError(error, res, requestId); } } /** * Handle streaming requests */ async handleStreamRequest(llmRequest, provider, res, requestId) { try { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Send start event res.write(`data: {"type": "message_start", "message": {"id": "${requestId}"}}\n\n`); // Use llm-interface streaming functionality const stream = await LLMInterface.sendMessage(provider, { ...llmRequest, stream: true }); // Handle streaming response for await (const chunk of stream) { const claudeChunk = this.claudeCompat.convertStreamResponse(chunk, provider); res.write(claudeChunk); } // Send end event res.write(`data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}}\n\n`); res.write(`data: [DONE]\n\n`); res.end(); } catch (error) { console.error(`❌ Streaming request failed [${requestId}]:`, error); res.write(`data: {"type": "error", "error": {"message": "${error.message}"}}\n\n`); res.end(); } } /** * Handle Claude chat completion requests */ async handleClaudeChat(req, res) { // Convert chat completion format to message format const claudeRequest = { model: req.body.model || 'claude-3-sonnet', messages: req.body.messages || [], max_tokens: req.body.max_tokens || 1000, temperature: req.body.temperature || 0.7, stream: req.body.stream || false }; // Reuse message processing logic req.body = claudeRequest; return this.handleClaudeMessages(req, res); } /** * Handle health check */ async handleHealth(req, res) { try { const status = this.providerRouter.getProviderStatus(); const healthyCount = Object.values(status).filter(p => p.healthy).length; const totalCount = Object.keys(status).length; res.json({ status: 'healthy', timestamp: new Date().toISOString(), providers: { total: totalCount, healthy: healthyCount, unhealthy: totalCount - healthyCount }, uptime: process.uptime(), memory: process.memoryUsage(), version: require('../package.json').version }); } catch (error) { res.status(500).json({ status: 'unhealthy', error: error.message }); } } /** * Handle provider status requests */ async handleProviders(req, res) { try { const status = this.providerRouter.getProviderStatus(); res.json({ providers: status, summary: { total: Object.keys(status).length, enabled: Object.values(status).filter(p => p.enabled).length, healthy: Object.values(status).filter(p => p.healthy).length } }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Handle configuration refresh requests */ async handleRefreshProviders(req, res) { try { console.log('πŸ”„ Manually refreshing provider configuration...'); await this.configManager.discoverProviders(); await this.setupDynamicProviders(); res.json({ success: true, message: 'Provider configuration refreshed', timestamp: new Date().toISOString(), total_providers: this.providers.size }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } } /** * Handle model list requests */ async handleModels(req, res) { try { const models = []; const claudeModels = this.claudeCompat.getSupportedClaudeModels(); for (const claudeModel of claudeModels) { models.push({ id: claudeModel, object: 'model', created: Date.now(), owned_by: 'claude-llm-gateway', providers: this.claudeCompat.getProviderModels('openai') // η€ΊδΎ‹ }); } res.json({ object: 'list', data: models }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Handle configuration requests */ async handleConfig(req, res) { try { const config = await this.configManager.loadConfig(); res.json(config); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Handle statistics requests */ async handleStats(req, res) { try { const stats = this.providerRouter.getStats(); res.json(stats); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Handle root path requests */ handleRoot(req, res) { res.json({ name: 'Claude LLM Gateway', version: require('../package.json').version, description: 'Multi-LLM API Gateway for Claude Code using llm-interface', endpoints: { messages: '/v1/messages', chat: '/v1/chat/completions', health: '/health', providers: '/providers', models: '/models', stats: '/stats' }, providers: Array.from(this.providers.keys()), documentation: 'https://github.com/claude-llm-gateway' }); } /** * Setup error handling */ setupErrorHandling() { // 404 handling this.app.use((req, res) => { res.status(404).json({ error: { type: 'not_found', message: `Endpoint ${req.path} not found` } }); }); // ε…¨ε±€Error handling this.app.use((error, req, res, next) => { console.error('🚨 Unhandled error:', error); res.status(500).json({ error: { type: 'internal_server_error', message: 'Internal server error', request_id: req.requestId } }); }); } /** * Handle request errors */ handleRequestError(error, res, requestId) { let status = 500; let errorType = 'internal_server_error'; let message = error.message; // Set status code based on error type if (error.message.includes('API key')) { status = 401; errorType = 'authentication_error'; } else if (error.message.includes('rate limit')) { status = 429; errorType = 'rate_limit_error'; } else if (error.message.includes('invalid')) { status = 400; errorType = 'invalid_request_error'; } this.logRequest(requestId, 'error', Date.now(), false, error.message); res.status(status).json({ error: { type: errorType, message: message, request_id: requestId } }); } /** * Log request */ logRequest(requestId, provider, duration, success, error = null) { this.requestLog.set(requestId, { timestamp: new Date().toISOString(), provider: provider, duration: duration, success: success, error: error }); // Keep log size reasonable if (this.requestLog.size > 1000) { const oldestKey = this.requestLog.keys().next().value; this.requestLog.delete(oldestKey); } } /** * Start server */ async start(port = null) { await this.initialize(); const serverPort = port || process.env.GATEWAY_PORT || 3000; const serverHost = process.env.GATEWAY_HOST || 'localhost'; this.app.listen(serverPort, serverHost, () => { console.log(`\n🌐 Claude LLM Gateway started successfully!`); console.log(`πŸ“‘ Service URL: http://${serverHost}:${serverPort}`); console.log(`πŸ”— Claude API: http://${serverHost}:${serverPort}/v1/messages`); console.log(`πŸ’¬ Chat API: http://${serverHost}:${serverPort}/v1/chat/completions`); console.log(`πŸ“Š Health Check: http://${serverHost}:${serverPort}/health`); console.log(`πŸ”§ Provider Status: http://${serverHost}:${serverPort}/providers`); console.log(`πŸ”„ Refresh Config: http://${serverHost}:${serverPort}/providers/refresh`); console.log(`πŸ“ˆ Statistics: http://${serverHost}:${serverPort}/stats`); }); } } // Start server if (require.main === module) { const gateway = new ClaudeLLMGateway(); gateway.start().catch(error => { console.error('❌ Server startup failed:', error); process.exit(1); }); } module.exports = ClaudeLLMGateway;