initial
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Failing after 5s
CI / Build All Platforms (push) Successful in 49s

This commit is contained in:
2026-01-30 03:16:57 +00:00
commit daaf6559e3
80 changed files with 14430 additions and 0 deletions

314
ts/cli/config-handler.ts Normal file
View File

@@ -0,0 +1,314 @@
/**
* 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;
}
}