315 lines
8.4 KiB
TypeScript
315 lines
8.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Config Handler
|
||
|
|
*
|
||
|
|
* CLI commands for configuration management.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { logger } from '../logger.ts';
|
||
|
|
import { theme } from '../colors.ts';
|
||
|
|
import { PATHS } from '../constants.ts';
|
||
|
|
import type { IModelGridConfig } from '../interfaces/config.ts';
|
||
|
|
import type { ITableColumn } from '../logger.ts';
|
||
|
|
import * as fs from 'node:fs/promises';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handler for configuration-related CLI commands
|
||
|
|
*/
|
||
|
|
export class ConfigHandler {
|
||
|
|
/**
|
||
|
|
* Show current configuration
|
||
|
|
*/
|
||
|
|
public async show(): Promise<void> {
|
||
|
|
logger.log('');
|
||
|
|
|
||
|
|
try {
|
||
|
|
const configPath = PATHS.CONFIG_FILE;
|
||
|
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
||
|
|
const config = JSON.parse(configContent) as IModelGridConfig;
|
||
|
|
|
||
|
|
// Overview
|
||
|
|
logger.logBox(
|
||
|
|
'ModelGrid Configuration',
|
||
|
|
[
|
||
|
|
`Version: ${theme.highlight(config.version)}`,
|
||
|
|
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
||
|
|
'',
|
||
|
|
theme.dim('Configuration File:'),
|
||
|
|
` ${theme.path(configPath)}`,
|
||
|
|
],
|
||
|
|
60,
|
||
|
|
'info',
|
||
|
|
);
|
||
|
|
|
||
|
|
// API Configuration
|
||
|
|
logger.log('');
|
||
|
|
logger.logBox(
|
||
|
|
'API Server',
|
||
|
|
[
|
||
|
|
`Host: ${theme.info(config.api.host)}`,
|
||
|
|
`Port: ${theme.highlight(String(config.api.port))}`,
|
||
|
|
`API Keys: ${config.api.apiKeys.length} configured`,
|
||
|
|
...(config.api.rateLimit
|
||
|
|
? [`Rate Limit: ${config.api.rateLimit} req/min`]
|
||
|
|
: []),
|
||
|
|
'',
|
||
|
|
theme.dim('Endpoint:'),
|
||
|
|
` http://${config.api.host}:${config.api.port}/v1/chat/completions`,
|
||
|
|
],
|
||
|
|
60,
|
||
|
|
'info',
|
||
|
|
);
|
||
|
|
|
||
|
|
// Docker Configuration
|
||
|
|
logger.log('');
|
||
|
|
logger.logBox(
|
||
|
|
'Docker',
|
||
|
|
[
|
||
|
|
`Runtime: ${theme.info(config.docker.runtime)}`,
|
||
|
|
`Network: ${config.docker.networkName}`,
|
||
|
|
],
|
||
|
|
60,
|
||
|
|
'default',
|
||
|
|
);
|
||
|
|
|
||
|
|
// GPU Configuration
|
||
|
|
logger.log('');
|
||
|
|
logger.logBox(
|
||
|
|
'GPU',
|
||
|
|
[
|
||
|
|
`Auto Detect: ${config.gpus.autoDetect ? theme.success('Yes') : theme.dim('No')}`,
|
||
|
|
`Assignments: ${Object.keys(config.gpus.assignments).length} GPU(s)`,
|
||
|
|
],
|
||
|
|
60,
|
||
|
|
'default',
|
||
|
|
);
|
||
|
|
|
||
|
|
// Model Configuration
|
||
|
|
logger.log('');
|
||
|
|
logger.logBox(
|
||
|
|
'Models',
|
||
|
|
[
|
||
|
|
`Auto Pull: ${config.models.autoPull ? theme.success('Enabled') : theme.dim('Disabled')}`,
|
||
|
|
`Default Container: ${config.models.defaultContainer}`,
|
||
|
|
`Auto Load: ${config.models.autoLoad.length} model(s)`,
|
||
|
|
'',
|
||
|
|
theme.dim('Greenlist URL:'),
|
||
|
|
` ${config.models.greenlistUrl}`,
|
||
|
|
],
|
||
|
|
70,
|
||
|
|
'default',
|
||
|
|
);
|
||
|
|
|
||
|
|
// Containers
|
||
|
|
if (config.containers.length > 0) {
|
||
|
|
logger.log('');
|
||
|
|
logger.info(`Containers (${config.containers.length}):`);
|
||
|
|
logger.log('');
|
||
|
|
|
||
|
|
const rows = config.containers.map((c) => ({
|
||
|
|
id: c.id,
|
||
|
|
name: c.name,
|
||
|
|
type: c.type,
|
||
|
|
image: c.image.length > 40 ? c.image.substring(0, 37) + '...' : c.image,
|
||
|
|
port: c.port,
|
||
|
|
gpus: c.gpuIds.length > 0 ? c.gpuIds.join(',') : theme.dim('None'),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const columns: ITableColumn[] = [
|
||
|
|
{ header: 'ID', key: 'id', align: 'left' },
|
||
|
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||
|
|
{ header: 'Type', key: 'type', align: 'left' },
|
||
|
|
{ header: 'Image', key: 'image', align: 'left', color: theme.dim },
|
||
|
|
{ header: 'Port', key: 'port', align: 'right' },
|
||
|
|
{ header: 'GPUs', key: 'gpus', align: 'left' },
|
||
|
|
];
|
||
|
|
|
||
|
|
logger.logTable(columns, rows);
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.log('');
|
||
|
|
} catch (error) {
|
||
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||
|
|
logger.logBox(
|
||
|
|
'No Configuration',
|
||
|
|
[
|
||
|
|
'No configuration file found.',
|
||
|
|
'',
|
||
|
|
theme.dim('Create configuration with:'),
|
||
|
|
` ${theme.command('modelgrid service enable')}`,
|
||
|
|
'',
|
||
|
|
theme.dim('Or manually create:'),
|
||
|
|
` ${PATHS.CONFIG_FILE}`,
|
||
|
|
],
|
||
|
|
60,
|
||
|
|
'warning',
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
logger.error(`Failed to read configuration: ${(error as Error).message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize default configuration
|
||
|
|
*/
|
||
|
|
public async init(): Promise<void> {
|
||
|
|
const configPath = PATHS.CONFIG_FILE;
|
||
|
|
|
||
|
|
// Check if config already exists
|
||
|
|
try {
|
||
|
|
await fs.access(configPath);
|
||
|
|
logger.warn('Configuration file already exists');
|
||
|
|
logger.dim(` ${configPath}`);
|
||
|
|
return;
|
||
|
|
} catch {
|
||
|
|
// File doesn't exist, continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create config directory
|
||
|
|
const configDir = PATHS.CONFIG_DIR;
|
||
|
|
await fs.mkdir(configDir, { recursive: true });
|
||
|
|
|
||
|
|
// Create default config
|
||
|
|
const defaultConfig: IModelGridConfig = {
|
||
|
|
version: '1.0.0',
|
||
|
|
api: {
|
||
|
|
port: 8080,
|
||
|
|
host: '0.0.0.0',
|
||
|
|
apiKeys: [],
|
||
|
|
cors: true,
|
||
|
|
corsOrigins: ['*'],
|
||
|
|
},
|
||
|
|
docker: {
|
||
|
|
networkName: 'modelgrid',
|
||
|
|
runtime: 'docker',
|
||
|
|
},
|
||
|
|
gpus: {
|
||
|
|
autoDetect: true,
|
||
|
|
assignments: {},
|
||
|
|
},
|
||
|
|
containers: [],
|
||
|
|
models: {
|
||
|
|
greenlistUrl: 'https://code.foss.global/modelgrid.com/model_lists/raw/branch/main/greenlit.json',
|
||
|
|
autoPull: true,
|
||
|
|
defaultContainer: 'ollama',
|
||
|
|
autoLoad: [],
|
||
|
|
},
|
||
|
|
checkInterval: 30000,
|
||
|
|
};
|
||
|
|
|
||
|
|
await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
|
||
|
|
|
||
|
|
logger.success('Configuration initialized');
|
||
|
|
logger.dim(` ${configPath}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add an API key
|
||
|
|
*/
|
||
|
|
public async addApiKey(key?: string): Promise<void> {
|
||
|
|
const configPath = PATHS.CONFIG_FILE;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
||
|
|
const config = JSON.parse(configContent) as IModelGridConfig;
|
||
|
|
|
||
|
|
// Generate key if not provided
|
||
|
|
const apiKey = key || this.generateApiKey();
|
||
|
|
|
||
|
|
if (config.api.apiKeys.includes(apiKey)) {
|
||
|
|
logger.warn('API key already exists');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
config.api.apiKeys.push(apiKey);
|
||
|
|
|
||
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
||
|
|
|
||
|
|
logger.success('API key added:');
|
||
|
|
logger.log(` ${theme.highlight(apiKey)}`);
|
||
|
|
logger.log('');
|
||
|
|
logger.dim('Use with Authorization header:');
|
||
|
|
logger.dim(` curl -H "Authorization: Bearer ${apiKey}" ...`);
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`Failed to add API key: ${(error as Error).message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Remove an API key
|
||
|
|
*/
|
||
|
|
public async removeApiKey(key: string): Promise<void> {
|
||
|
|
if (!key) {
|
||
|
|
logger.error('API key is required');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const configPath = PATHS.CONFIG_FILE;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
||
|
|
const config = JSON.parse(configContent) as IModelGridConfig;
|
||
|
|
|
||
|
|
const index = config.api.apiKeys.indexOf(key);
|
||
|
|
if (index === -1) {
|
||
|
|
logger.warn('API key not found');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
config.api.apiKeys.splice(index, 1);
|
||
|
|
|
||
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
||
|
|
|
||
|
|
logger.success('API key removed');
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`Failed to remove API key: ${(error as Error).message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* List API keys
|
||
|
|
*/
|
||
|
|
public async listApiKeys(): Promise<void> {
|
||
|
|
const configPath = PATHS.CONFIG_FILE;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
||
|
|
const config = JSON.parse(configContent) as IModelGridConfig;
|
||
|
|
|
||
|
|
if (config.api.apiKeys.length === 0) {
|
||
|
|
logger.warn('No API keys configured');
|
||
|
|
logger.dim('Add a key with: modelgrid config apikey add');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`API Keys (${config.api.apiKeys.length}):`);
|
||
|
|
logger.log('');
|
||
|
|
|
||
|
|
for (const key of config.api.apiKeys) {
|
||
|
|
// Show partial key for security
|
||
|
|
const masked = key.substring(0, 8) + '...' + key.substring(key.length - 4);
|
||
|
|
logger.log(` ${masked}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.log('');
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`Failed to list API keys: ${(error as Error).message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a random API key
|
||
|
|
*/
|
||
|
|
private generateApiKey(): string {
|
||
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
|
|
const length = 48;
|
||
|
|
let key = 'sk-';
|
||
|
|
|
||
|
|
for (let i = 0; i < length; i++) {
|
||
|
|
key += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
|
|
}
|
||
|
|
|
||
|
|
return key;
|
||
|
|
}
|
||
|
|
}
|