| import { promises as fs } from 'node:fs'; |
| import path from 'node:path'; |
| import { spawn } from 'node:child_process'; |
|
|
| export class ModelManager { |
| constructor({ manifestPath, modelDir, binaryPath, spawnProcess = spawn } = {}) { |
| this.manifestPath = manifestPath; |
| this.modelDir = path.resolve(modelDir); |
| this.binaryPath = binaryPath; |
| this.spawnProcess = spawnProcess; |
| this.models = new Map(); |
| this.processes = new Map(); |
| } |
|
|
| async load() { |
| const manifest = JSON.parse(await fs.readFile(this.manifestPath, 'utf8')); |
| this.models = new Map(manifest.models.map(model => [model.id, model])); |
| return manifest; |
| } |
|
|
| status() { |
| return [...this.models.values()].map(model => ({ |
| id: model.id, |
| name: model.name, |
| status: this.processes.has(model.id) ? 'running' : model.status, |
| endpoint: model.endpoint |
| })); |
| } |
|
|
| async start(id) { |
| const model = this.models.get(id); |
| if (!model) throw new Error('model is not allowlisted'); |
| if (this.processes.has(id)) return { id, status: 'running', endpoint: model.endpoint }; |
| if (!this.binaryPath) throw new Error('llama-server binary is not configured'); |
| await fs.access(this.binaryPath).catch(() => { throw new Error('llama-server binary is unavailable'); }); |
| const file = path.resolve(this.modelDir, path.basename(model.artifact_uri.replace('local://', ''))); |
| if (!file.startsWith(`${this.modelDir}${path.sep}`)) throw new Error('model path escaped model directory'); |
| await fs.access(file); |
| await this.stopAll(); |
| const endpoint = new URL(model.endpoint); |
| const args = ['-m', file, '--host', '127.0.0.1', '--port', String(endpoint.port || 8080), '--ctx-size', String(model.context_tokens || 2048), '--threads', '4', '--parallel', '1', '--log-disable']; |
| const child = this.spawnProcess(this.binaryPath, args, { stdio: 'ignore' }); |
| this.processes.set(id, child); |
| child.once('exit', () => this.processes.delete(id)); |
| return { id, status: 'starting', endpoint: model.endpoint }; |
| } |
|
|
| async stop(id) { |
| const child = this.processes.get(id); |
| if (!child) return { id, status: 'stopped' }; |
| child.kill('SIGTERM'); |
| this.processes.delete(id); |
| return { id, status: 'stopped' }; |
| } |
|
|
| async stopAll() { |
| for (const id of [...this.processes.keys()]) await this.stop(id); |
| } |
| } |
|
|