/** * Container Manager * * Orchestrates multiple AI model containers. */ import type { IContainerConfig, IContainerStatus, IContainerEndpoint, TContainerType, } from '../interfaces/container.ts'; import { logger } from '../logger.ts'; import { DockerManager } from '../docker/docker-manager.ts'; import { BaseContainer } from './base-container.ts'; import { OllamaContainer } from './ollama.ts'; import { VllmContainer } from './vllm.ts'; import { TgiContainer } from './tgi.ts'; /** * Container Manager - orchestrates all containers */ export class ContainerManager { private containers: Map; private dockerManager: DockerManager; constructor() { this.containers = new Map(); this.dockerManager = new DockerManager(); } /** * Initialize container manager */ public async initialize(): Promise { // Ensure Docker is running if (!await this.dockerManager.isRunning()) { throw new Error('Docker is not running'); } // Create network if it doesn't exist await this.dockerManager.createNetwork(); } /** * Create a container instance from config */ private createContainerInstance(config: IContainerConfig): BaseContainer { switch (config.type) { case 'ollama': return new OllamaContainer(config); case 'vllm': return new VllmContainer(config); case 'tgi': return new TgiContainer(config); default: throw new Error(`Unknown container type: ${config.type}`); } } /** * Add a container */ public addContainer(config: IContainerConfig): BaseContainer { if (this.containers.has(config.id)) { throw new Error(`Container with ID ${config.id} already exists`); } const container = this.createContainerInstance(config); this.containers.set(config.id, container); return container; } /** * Remove a container */ public async removeContainer(containerId: string): Promise { const container = this.containers.get(containerId); if (!container) { return false; } await container.remove(); this.containers.delete(containerId); return true; } /** * Get a container by ID */ public getContainer(containerId: string): BaseContainer | undefined { return this.containers.get(containerId); } /** * Get all containers */ public getAllContainers(): BaseContainer[] { return Array.from(this.containers.values()); } /** * Load containers from configuration */ public loadFromConfig(configs: IContainerConfig[]): void { this.containers.clear(); for (const config of configs) { try { this.addContainer(config); } catch (error) { logger.warn(`Failed to load container ${config.id}: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Start all containers */ public async startAll(): Promise> { const results = new Map(); for (const [id, container] of this.containers) { if (!container.getConfig().autoStart) { continue; } try { const success = await container.start(); results.set(id, success); } catch (error) { logger.error(`Failed to start container ${id}: ${error instanceof Error ? error.message : String(error)}`); results.set(id, false); } } return results; } /** * Stop all containers */ public async stopAll(): Promise> { const results = new Map(); for (const [id, container] of this.containers) { try { const success = await container.stop(); results.set(id, success); } catch (error) { logger.error(`Failed to stop container ${id}: ${error instanceof Error ? error.message : String(error)}`); results.set(id, false); } } return results; } /** * Get status of all containers */ public async getAllStatus(): Promise> { const statuses = new Map(); for (const [id, container] of this.containers) { try { const status = await container.getStatus(); statuses.set(id, status); } catch (error) { logger.warn(`Failed to get status for container ${id}: ${error instanceof Error ? error.message : String(error)}`); } } return statuses; } /** * Get available endpoints for a model */ public async getEndpointsForModel(modelName: string): Promise { const endpoints: IContainerEndpoint[] = []; for (const [_id, container] of this.containers) { try { const status = await container.getStatus(); if (!status.running) { continue; } // Check if container has this model const models = await container.listModels(); if (!models.includes(modelName)) { continue; } endpoints.push({ containerId: container.getConfig().id, type: container.type, url: container.getEndpoint(), models, healthy: status.health === 'healthy', priority: 0, // Could be based on load }); } catch { // Skip containers that fail to respond } } return endpoints; } /** * Find best container for a model */ public async findContainerForModel(modelName: string): Promise { const endpoints = await this.getEndpointsForModel(modelName); // Filter to healthy endpoints const healthy = endpoints.filter((e) => e.healthy); if (healthy.length === 0) { return null; } // Return first healthy endpoint (could add load balancing) const endpoint = healthy[0]; return this.containers.get(endpoint.containerId) || null; } /** * Get all available models across all containers */ public async getAllAvailableModels(): Promise> { const modelMap = new Map(); for (const container of this.containers.values()) { try { const status = await container.getStatus(); if (!status.running) continue; const models = await container.listModels(); for (const model of models) { if (!modelMap.has(model)) { modelMap.set(model, []); } modelMap.get(model)!.push({ containerId: container.getConfig().id, type: container.type, url: container.getEndpoint(), models, healthy: status.health === 'healthy', priority: 0, }); } } catch { // Skip failed containers } } return modelMap; } /** * Pull a model to a specific container type */ public async pullModel( modelName: string, containerType: TContainerType = 'ollama', containerId?: string, ): Promise { // Find or create appropriate container let container: BaseContainer | undefined; if (containerId) { container = this.containers.get(containerId); } else { // Find first container of the specified type for (const c of this.containers.values()) { if (c.type === containerType) { container = c; break; } } } if (!container) { logger.error(`No ${containerType} container available to pull model`); return false; } return container.pullModel(modelName, (progress) => { const percent = progress.percent !== undefined ? ` (${progress.percent}%)` : ''; logger.dim(` ${progress.status}${percent}`); }); } /** * Health check all containers */ public async healthCheck(): Promise> { const results = new Map(); for (const [id, container] of this.containers) { try { const healthy = await container.isHealthy(); results.set(id, healthy); } catch { results.set(id, false); } } return results; } /** * Print container status summary */ public async printStatus(): Promise { const statuses = await this.getAllStatus(); if (statuses.size === 0) { logger.logBox('Containers', ['No containers configured'], 50, 'warning'); return; } logger.logBoxTitle('Container Status', 70, 'info'); for (const [id, status] of statuses) { const runningStr = status.running ? 'Running' : 'Stopped'; const healthStr = status.health; const modelsStr = status.loadedModels.length > 0 ? status.loadedModels.join(', ') : 'None'; logger.logBoxLine(`${status.name} (${id})`); logger.logBoxLine(` Type: ${status.type} | Status: ${runningStr} | Health: ${healthStr}`); logger.logBoxLine(` Models: ${modelsStr}`); logger.logBoxLine(` Endpoint: ${status.endpoint}`); if (status.gpuUtilization !== undefined) { logger.logBoxLine(` GPU: ${status.gpuUtilization}% | Memory: ${status.memoryUsage || 0}MB`); } logger.logBoxLine(''); } logger.logBoxEnd(); } }