/** * API Router * * Routes incoming requests to appropriate handlers. */ import * as http from 'node:http'; import type { IApiError, IChatCompletionRequest } from '../interfaces/api.ts'; import { ClusterCoordinator } from '../cluster/coordinator.ts'; import { logger } from '../logger.ts'; import { ContainerManager } from '../containers/container-manager.ts'; import { ModelRegistry } from '../models/registry.ts'; import { ModelLoader } from '../models/loader.ts'; import { ChatHandler } from './handlers/chat.ts'; import { ModelsHandler } from './handlers/models.ts'; import { EmbeddingsHandler } from './handlers/embeddings.ts'; import { AuthMiddleware } from './middleware/auth.ts'; import { SanityMiddleware } from './middleware/sanity.ts'; interface IParsedRequestBody { kind: 'ok' | 'invalid' | 'too_large'; body?: unknown; } interface IApiRouterOptions { chatHandler?: ChatHandler; modelsHandler?: ModelsHandler; embeddingsHandler?: EmbeddingsHandler; authMiddleware?: AuthMiddleware; sanityMiddleware?: SanityMiddleware; } /** * API Router - routes requests to handlers */ export class ApiRouter { private containerManager: ContainerManager; private modelRegistry: ModelRegistry; private modelLoader: ModelLoader; private clusterCoordinator: ClusterCoordinator; private chatHandler: ChatHandler; private modelsHandler: ModelsHandler; private embeddingsHandler: EmbeddingsHandler; private authMiddleware: AuthMiddleware; private sanityMiddleware: SanityMiddleware; constructor( containerManager: ContainerManager, modelRegistry: ModelRegistry, modelLoader: ModelLoader, clusterCoordinator: ClusterCoordinator, apiKeys: string[], options: IApiRouterOptions = {}, ) { this.containerManager = containerManager; this.modelRegistry = modelRegistry; this.modelLoader = modelLoader; this.clusterCoordinator = clusterCoordinator; // Initialize handlers this.chatHandler = options.chatHandler || new ChatHandler( containerManager, modelRegistry, modelLoader, clusterCoordinator, ); this.modelsHandler = options.modelsHandler || new ModelsHandler(containerManager, modelRegistry, clusterCoordinator); this.embeddingsHandler = options.embeddingsHandler || new EmbeddingsHandler( containerManager, modelRegistry, clusterCoordinator, ); // Initialize middleware this.authMiddleware = options.authMiddleware || new AuthMiddleware(apiKeys); this.sanityMiddleware = options.sanityMiddleware || new SanityMiddleware(modelRegistry); } /** * Route a request to the appropriate handler */ public async route( req: http.IncomingMessage, res: http.ServerResponse, path: string, ): Promise { // OpenAI API endpoints if (path === '/v1/chat/completions') { await this.handleChatCompletions(req, res); return; } if (path === '/v1/completions') { await this.handleCompletions(req, res); return; } if (path === '/v1/models' || path === '/v1/models/') { await this.handleModels(req, res); return; } if (path.startsWith('/v1/models/')) { await this.handleModelInfo(req, res, path); return; } if (path === '/v1/embeddings') { await this.handleEmbeddings(req, res); return; } // Not found this.sendError(res, 404, `Endpoint not found: ${path}`, 'invalid_request_error'); } /** * Handle POST /v1/chat/completions */ private async handleChatCompletions( req: http.IncomingMessage, res: http.ServerResponse, ): Promise { if (req.method !== 'POST') { this.sendError(res, 405, 'Method not allowed', 'invalid_request_error'); return; } // Authenticate if (!this.authMiddleware.authenticate(req)) { this.sendError(res, 401, 'Invalid API key', 'authentication_error'); return; } // Parse body const parsedBody = await this.parseRequestBody(req); if (parsedBody.kind === 'too_large') { this.sendError(res, 413, 'Request body too large', 'invalid_request_error'); return; } if (parsedBody.kind !== 'ok') { this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error'); return; } const body = parsedBody.body; // Validate request const validation = this.sanityMiddleware.validateChatRequest(body); if (!validation.valid) { this.sendError(res, 400, validation.error || 'Invalid request', 'invalid_request_error'); return; } const requestBody = this.sanityMiddleware.sanitizeChatRequest(body as Record); await this.chatHandler.handleChatCompletion(req, res, requestBody); } /** * Handle POST /v1/completions (legacy endpoint) */ private async handleCompletions( req: http.IncomingMessage, res: http.ServerResponse, ): Promise { if (req.method !== 'POST') { this.sendError(res, 405, 'Method not allowed', 'invalid_request_error'); return; } // Authenticate if (!this.authMiddleware.authenticate(req)) { this.sendError(res, 401, 'Invalid API key', 'authentication_error'); return; } // Parse body const parsedBody = await this.parseRequestBody(req); if (parsedBody.kind === 'too_large') { this.sendError(res, 413, 'Request body too large', 'invalid_request_error'); return; } if (parsedBody.kind !== 'ok') { this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error'); return; } const body = parsedBody.body; // Convert to chat format and handle const chatBody = this.convertCompletionToChat(body as Record); await this.chatHandler.handleChatCompletion(req, res, chatBody); } /** * Handle GET /v1/models */ private async handleModels( req: http.IncomingMessage, res: http.ServerResponse, ): Promise { if (req.method !== 'GET') { this.sendError(res, 405, 'Method not allowed', 'invalid_request_error'); return; } // Authenticate if (!this.authMiddleware.authenticate(req)) { this.sendError(res, 401, 'Invalid API key', 'authentication_error'); return; } await this.modelsHandler.handleListModels(res); } /** * Handle GET /v1/models/:model */ private async handleModelInfo( req: http.IncomingMessage, res: http.ServerResponse, path: string, ): Promise { if (req.method !== 'GET') { this.sendError(res, 405, 'Method not allowed', 'invalid_request_error'); return; } // Authenticate if (!this.authMiddleware.authenticate(req)) { this.sendError(res, 401, 'Invalid API key', 'authentication_error'); return; } const modelId = path.replace('/v1/models/', ''); await this.modelsHandler.handleGetModel(res, modelId); } /** * Handle POST /v1/embeddings */ private async handleEmbeddings( req: http.IncomingMessage, res: http.ServerResponse, ): Promise { if (req.method !== 'POST') { this.sendError(res, 405, 'Method not allowed', 'invalid_request_error'); return; } // Authenticate if (!this.authMiddleware.authenticate(req)) { this.sendError(res, 401, 'Invalid API key', 'authentication_error'); return; } // Parse body const parsedBody = await this.parseRequestBody(req); if (parsedBody.kind === 'too_large') { this.sendError(res, 413, 'Request body too large', 'invalid_request_error'); return; } if (parsedBody.kind !== 'ok') { this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error'); return; } const body = parsedBody.body; const validation = this.sanityMiddleware.validateEmbeddingsRequest(body); if (!validation.valid) { this.sendError(res, 400, validation.error || 'Invalid request', 'invalid_request_error'); return; } const requestBody = this.sanityMiddleware.sanitizeEmbeddingsRequest( body as Record, ); await this.embeddingsHandler.handleEmbeddings(req, res, requestBody); } /** * Parse request body */ private async parseRequestBody(req: http.IncomingMessage): Promise { return new Promise((resolve) => { let body = ''; let resolved = false; const finish = (result: IParsedRequestBody): void => { if (resolved) { return; } resolved = true; resolve(result); }; req.on('data', (chunk) => { if (resolved) { return; } body += chunk.toString(); if (body.length > 10 * 1024 * 1024) { req.pause(); req.destroy(); finish({ kind: 'too_large' }); } }); req.on('end', () => { try { finish({ kind: 'ok', body: JSON.parse(body) }); } catch { finish({ kind: 'invalid' }); } }); req.on('error', () => { if (!resolved) { finish({ kind: 'invalid' }); } }); }); } /** * Convert legacy completion request to chat format */ private convertCompletionToChat(body: Record): IChatCompletionRequest { const prompt = body.prompt as string | string[]; const promptText = Array.isArray(prompt) ? prompt.join('\n') : prompt; return { model: body.model as string, messages: [ { role: 'user', content: promptText }, ], max_tokens: body.max_tokens as number | undefined, temperature: body.temperature as number | undefined, top_p: body.top_p as number | undefined, n: body.n as number | undefined, stream: body.stream as boolean | undefined, stop: body.stop as string | string[] | undefined, }; } /** * Send error response */ private sendError( res: http.ServerResponse, statusCode: number, message: string, type: string, param?: string, ): void { const error: IApiError = { error: { message, type, param, }, }; res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(error)); } }