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
JavaScript
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;