claude-llm-gateway
Version:
๐ง Intelligent API gateway with automatic model selection - connects Claude Code to 36+ LLM providers with smart task detection and cost optimization
514 lines (431 loc) โข 16.7 kB
JavaScript
/**
* Multi-LLM Gateway CLI
* Command-line interface for the Claude LLM Gateway
*/
const { Command } = require('commander');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const ClaudeLLMGateway = require('../src/server');
const DynamicConfigManager = require('../src/config/dynamic-config-manager');
const packageJson = require('../package.json');
// Helper function to display providers summary
async function displayProvidersSummary(host, port) {
try {
const fetch = require('node-fetch');
const response = await fetch(`http://${host}:${port}/providers`);
if (!response.ok) {
console.log(chalk.yellow('โ ๏ธ Could not fetch providers status'));
return;
}
const data = await response.json();
const providers = data.providers;
// Categorize providers
const healthy = [];
const needApiKey = [];
const unreachable = [];
const otherIssues = [];
const notTested = [];
Object.entries(providers).forEach(([name, info]) => {
if (info.healthy) {
healthy.push({ name, responseTime: info.response_time });
} else if (info.status_type === 'no_api_key') {
needApiKey.push({ name, error: info.error });
} else if (info.status_type === 'unreachable') {
unreachable.push({ name, error: info.error });
} else if (info.status_type === 'unhealthy') {
otherIssues.push({ name, error: info.error });
} else {
notTested.push({ name });
}
});
console.log(chalk.blue('\n๐ === Providers Status Summary ==='));
console.log(chalk.gray(`Total: ${Object.keys(providers).length} providers\n`));
if (healthy.length > 0) {
console.log(chalk.green(`โ
Healthy Providers (${healthy.length}):`));
healthy.forEach(p => {
console.log(` ${chalk.green('โ')} ${p.name}: ${p.responseTime}ms`);
});
console.log('');
}
if (needApiKey.length > 0) {
console.log(chalk.yellow(`๐ Need API Key (${needApiKey.length}):`));
needApiKey.forEach(p => {
console.log(` ${chalk.yellow('โ')} ${p.name}: ${p.error}`);
});
console.log('');
}
if (unreachable.length > 0) {
console.log(chalk.red(`๐ Unreachable (${unreachable.length}):`));
unreachable.forEach(p => {
console.log(` ${chalk.red('โ')} ${p.name}: ${p.error}`);
});
console.log('');
}
if (otherIssues.length > 0) {
console.log(chalk.red(`โ Other Issues (${otherIssues.length}):`));
otherIssues.forEach(p => {
console.log(` ${chalk.red('โ')} ${p.name}: ${p.error}`);
});
console.log('');
}
if (notTested.length > 0) {
console.log(chalk.gray(`โช Not Tested (${notTested.length}):`));
// Show first 5, then summary
notTested.slice(0, 5).forEach(p => {
console.log(` ${chalk.gray('โ')} ${p.name}: Not configured`);
});
if (notTested.length > 5) {
console.log(` ${chalk.gray('...')} and ${notTested.length - 5} more providers`);
}
console.log('');
}
console.log(chalk.blue('๐ Service URLs:'));
console.log(` Claude API: http://${host}:${port}/v1/messages`);
console.log(` Health Check: http://${host}:${port}/health`);
console.log(` Provider Status: http://${host}:${port}/providers`);
console.log('');
} catch (error) {
console.log(chalk.yellow(`โ ๏ธ Could not display providers summary: ${error.message}`));
}
}
// Daemon process management
async function startDaemon(options) {
const logDir = path.join(__dirname, '..', 'logs');
const pidFile = path.join(logDir, 'gateway.pid');
const logFile = path.join(logDir, 'gateway.log');
// Ensure log directory exists
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// Check if daemon is already running
if (fs.existsSync(pidFile)) {
const pid = fs.readFileSync(pidFile, 'utf8').trim();
try {
// Check if process is still running
process.kill(pid, 0);
console.log(chalk.yellow(`โ ๏ธ Gateway daemon is already running (PID: ${pid})`));
return;
} catch (e) {
// Process is not running, remove stale PID file
fs.unlinkSync(pidFile);
}
}
console.log(chalk.blue('๐ Starting daemon mode...'));
// Check if we're already in daemon mode
if (process.env.DAEMON_MODE === 'true') {
return await startDaemonProcess(options);
}
// Use spawn instead of fork for better control
const { spawn } = require('child_process');
const child = spawn(process.execPath, [__filename, 'start', '--port', options.port, '--host', options.host || 'localhost'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
env: {
...process.env,
DAEMON_MODE: 'true',
LOG_LEVEL: options.debug ? 'debug' : process.env.LOG_LEVEL,
CONFIG_PATH: options.config || process.env.CONFIG_PATH
}
});
// Save PID
fs.writeFileSync(pidFile, child.pid.toString());
// Detach the child process
child.unref();
console.log(chalk.green(`โ
Gateway daemon started successfully (PID: ${child.pid})`));
console.log(chalk.blue(`๐ก Service URL: http://${options.host || 'localhost'}:${options.port}`));
console.log(chalk.blue(`๐ Claude API: http://${options.host || 'localhost'}:${options.port}/v1/messages`));
console.log(chalk.blue(`๐ฌ Chat API: http://${options.host || 'localhost'}:${options.port}/v1/chat/completions`));
console.log(chalk.blue(`๐ Health Check: http://${options.host || 'localhost'}:${options.port}/health`));
console.log(chalk.blue(`๐ Logs: ${logFile}`));
console.log(chalk.blue(`๐ PID file: ${pidFile}`));
console.log(chalk.yellow(`\n๐ก Use 'claude-llm-gateway stop' to stop the daemon`));
console.log(chalk.gray(`\nโฑ๏ธ Starting up... please wait 10-20 seconds for full initialization`));
// Show providers summary after daemon starts
setTimeout(async () => {
console.log(chalk.blue('\n๐ Fetching providers status...'));
await displayProvidersSummary(options.host || 'localhost', options.port);
}, 15000);
process.exit(0);
}
async function startDaemonProcess(options) {
try {
if (options.debug) {
process.env.LOG_LEVEL = 'debug';
}
if (options.config) {
process.env.CONFIG_PATH = options.config;
}
const gateway = new ClaudeLLMGateway();
await gateway.start(parseInt(options.port), options.host);
// Notify parent process BEFORE redirecting output
if (process.send) {
process.send({
type: 'started',
pid: process.pid,
port: options.port,
host: options.host || 'localhost'
});
}
// Now redirect stdout/stderr to log file
const logDir = path.join(__dirname, '..', 'logs');
const logFile = path.join(logDir, 'gateway.log');
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
process.stdout.write = process.stderr.write = logStream.write.bind(logStream);
console.log(`\n=== Gateway Daemon Started at ${new Date().toISOString()} ===`);
console.log(`โ
Gateway daemon running on ${options.host}:${options.port}`);
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('๐ Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('๐ Received SIGINT, shutting down gracefully...');
process.exit(0);
});
} catch (error) {
console.error('โ Daemon startup failed:', error.message);
if (process.send) {
process.send({ type: 'error', error: error.message });
}
process.exit(1);
}
}
const program = new Command();
program
.name('multi-llm-gateway')
.description('CLI for Claude LLM Gateway')
.version(packageJson.version);
// Start command
program
.command('start')
.description('Start the Claude LLM Gateway')
.option('-p, --port <port>', 'Port number to run the gateway on', '3000')
.option('-h, --host <host>', 'Host to bind the gateway to', 'localhost')
.option('-c, --config <path>', 'Path to configuration file')
.option('-d, --daemon', 'Run as daemon process')
.option('--debug', 'Enable debug logging')
.action(async (options) => {
try {
// Handle daemon mode
if (options.daemon) {
return await startDaemon(options);
}
console.log(chalk.blue('๐ Starting Claude LLM Gateway...'));
if (options.debug) {
process.env.LOG_LEVEL = 'debug';
}
if (options.config) {
process.env.CONFIG_PATH = options.config;
}
const gateway = new ClaudeLLMGateway();
await gateway.start(parseInt(options.port), options.host);
console.log(chalk.green(`โ
Gateway started successfully on ${options.host}:${options.port}`));
// Wait a moment for health checks to complete
setTimeout(async () => {
await displayProvidersSummary(options.host, options.port);
}, 5000);
// Keep process alive in foreground mode
process.on('SIGINT', () => {
console.log(chalk.yellow('\n๐ Shutting down gracefully...'));
process.exit(0);
});
} catch (error) {
console.error(chalk.red('โ Failed to start gateway:'), error.message);
process.exit(1);
}
});
// Stop command
program
.command('stop')
.description('Stop the daemon gateway')
.action(async () => {
try {
const logDir = path.join(__dirname, '..', 'logs');
const pidFile = path.join(logDir, 'gateway.pid');
if (!fs.existsSync(pidFile)) {
console.log(chalk.yellow('โ ๏ธ No daemon process found'));
return;
}
const pid = fs.readFileSync(pidFile, 'utf8').trim();
try {
// Check if process is running
process.kill(pid, 0);
console.log(chalk.blue(`๐ Stopping daemon process (PID: ${pid})...`));
// Send SIGTERM for graceful shutdown
process.kill(pid, 'SIGTERM');
// Wait a bit and check if process stopped
setTimeout(() => {
try {
process.kill(pid, 0);
console.log(chalk.yellow('โ ๏ธ Process still running, sending SIGKILL...'));
process.kill(pid, 'SIGKILL');
} catch (e) {
// Process stopped
}
// Remove PID file
if (fs.existsSync(pidFile)) {
fs.unlinkSync(pidFile);
}
console.log(chalk.green('โ
Daemon stopped successfully'));
}, 5000);
} catch (e) {
// Process not running
console.log(chalk.yellow('โ ๏ธ Daemon process not running'));
fs.unlinkSync(pidFile);
}
} catch (error) {
console.error(chalk.red('โ Failed to stop daemon:'), error.message);
process.exit(1);
}
});
// Config command
program
.command('config')
.description('Manage gateway configuration')
.option('-u, --update', 'Update provider configuration')
.option('-s, --show', 'Show current configuration')
.option('-r, --reset', 'Reset configuration to defaults')
.action(async (options) => {
try {
const configManager = new DynamicConfigManager();
if (options.update) {
console.log(chalk.blue('๐ Updating provider configuration...'));
await configManager.discoverProviders();
console.log(chalk.green('โ
Configuration updated successfully'));
}
if (options.show) {
console.log(chalk.blue('๐ Current Configuration:'));
const config = await configManager.loadConfig();
if (config) {
console.log(JSON.stringify(config, null, 2));
} else {
console.log(chalk.yellow('โ ๏ธ No configuration found'));
}
}
if (options.reset) {
console.log(chalk.yellow('๐ Resetting configuration...'));
// Implementation would remove config file and regenerate
console.log(chalk.green('โ
Configuration reset'));
}
} catch (error) {
console.error(chalk.red('โ Configuration operation failed:'), error.message);
process.exit(1);
}
});
// Test command
program
.command('test')
.description('Test gateway functionality')
.option('-p, --provider <provider>', 'Test specific provider')
.option('-m, --model <model>', 'Test specific model')
.option('-u, --url <url>', 'Gateway URL to test', 'http://localhost:3000')
.action(async (options) => {
try {
console.log(chalk.blue('๐งช Testing gateway functionality...'));
const testMessage = {
model: options.model || 'claude-3-sonnet',
messages: [
{ role: 'user', content: 'Hello! This is a test message.' }
],
max_tokens: 50
};
const fetch = require('node-fetch');
const response = await fetch(`${options.url}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testMessage)
});
if (response.ok) {
const result = await response.json();
console.log(chalk.green('โ
Test successful!'));
console.log('Response:', JSON.stringify(result, null, 2));
} else {
console.log(chalk.red('โ Test failed:'), response.status, response.statusText);
}
} catch (error) {
console.error(chalk.red('โ Test failed:'), error.message);
process.exit(1);
}
});
// Status command
program
.command('status')
.description('Check gateway status and show providers summary')
.option('-u, --url <url>', 'Gateway URL to check', 'http://localhost:8765')
.action(async (options) => {
try {
const fetch = require('node-fetch');
console.log(chalk.blue('๐ Checking gateway status...'));
// Health check
const healthResponse = await fetch(`${options.url}/health`);
if (healthResponse.ok) {
const health = await healthResponse.json();
console.log(chalk.green('โ
Gateway is healthy'));
console.log(`Uptime: ${Math.floor(health.uptime / 3600)}h ${Math.floor((health.uptime % 3600) / 60)}m`);
console.log(`Providers: ${health.providers.healthy}/${health.providers.total} healthy`);
} else {
console.log(chalk.red('โ Gateway is not responding'));
process.exit(1);
}
// Use the shared function to display providers summary
const url = new URL(options.url);
await displayProvidersSummary(url.hostname, url.port);
} catch (error) {
console.error(chalk.red('โ Status check failed:'), error.message);
process.exit(1);
}
});
// Install command
program
.command('install')
.description('Install and configure the gateway')
.option('-d, --directory <dir>', 'Installation directory', './multi-llm-gateway')
.action(async (options) => {
try {
console.log(chalk.blue('๐ฆ Installing Multi-LLM Gateway...'));
const installDir = path.resolve(options.directory);
// Create directory structure
const dirs = ['config', 'logs', 'scripts'];
for (const dir of dirs) {
const dirPath = path.join(installDir, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// Copy environment example
const envExample = path.join(__dirname, '../env.example');
const envTarget = path.join(installDir, '.env.example');
if (fs.existsSync(envExample)) {
fs.copyFileSync(envExample, envTarget);
}
console.log(chalk.green(`โ
Gateway installed in ${installDir}`));
console.log(chalk.yellow('Next steps:'));
console.log(`1. cd ${installDir}`);
console.log('2. cp .env.example .env');
console.log('3. Edit .env with your API keys');
console.log('4. multi-llm-gateway start');
} catch (error) {
console.error(chalk.red('โ Installation failed:'), error.message);
process.exit(1);
}
});
// Version command (already handled by commander, but we can customize it)
program
.command('version')
.description('Show version information')
.action(() => {
console.log(chalk.blue('Claude LLM Gateway'));
console.log(`Version: ${packageJson.version}`);
console.log(`Node: ${process.version}`);
console.log(`Platform: ${process.platform} ${process.arch}`);
});
// Parse command line arguments
program.parse(process.argv);
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
}