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

300
ts/api/router.ts Normal file
View File

@@ -0,0 +1,300 @@
/**
* API Router
*
* Routes incoming requests to appropriate handlers.
*/
import * as http from 'node:http';
import type { IApiError } from '../interfaces/api.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';
/**
* API Router - routes requests to handlers
*/
export class ApiRouter {
private containerManager: ContainerManager;
private modelRegistry: ModelRegistry;
private modelLoader: ModelLoader;
private chatHandler: ChatHandler;
private modelsHandler: ModelsHandler;
private embeddingsHandler: EmbeddingsHandler;
private authMiddleware: AuthMiddleware;
private sanityMiddleware: SanityMiddleware;
constructor(
containerManager: ContainerManager,
modelRegistry: ModelRegistry,
modelLoader: ModelLoader,
apiKeys: string[],
) {
this.containerManager = containerManager;
this.modelRegistry = modelRegistry;
this.modelLoader = modelLoader;
// Initialize handlers
this.chatHandler = new ChatHandler(containerManager, modelLoader);
this.modelsHandler = new ModelsHandler(containerManager, modelRegistry);
this.embeddingsHandler = new EmbeddingsHandler(containerManager);
// Initialize middleware
this.authMiddleware = new AuthMiddleware(apiKeys);
this.sanityMiddleware = new SanityMiddleware(modelRegistry);
}
/**
* Route a request to the appropriate handler
*/
public async route(
req: http.IncomingMessage,
res: http.ServerResponse,
path: string,
): Promise<void> {
// 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<void> {
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 body = await this.parseRequestBody(req);
if (!body) {
this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error');
return;
}
// Validate request
const validation = this.sanityMiddleware.validateChatRequest(body);
if (!validation.valid) {
this.sendError(res, 400, validation.error || 'Invalid request', 'invalid_request_error');
return;
}
// Handle request
await this.chatHandler.handleChatCompletion(req, res, body);
}
/**
* Handle POST /v1/completions (legacy endpoint)
*/
private async handleCompletions(
req: http.IncomingMessage,
res: http.ServerResponse,
): Promise<void> {
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 body = await this.parseRequestBody(req);
if (!body) {
this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error');
return;
}
// Convert to chat format and handle
const chatBody = this.convertCompletionToChat(body);
await this.chatHandler.handleChatCompletion(req, res, chatBody);
}
/**
* Handle GET /v1/models
*/
private async handleModels(
req: http.IncomingMessage,
res: http.ServerResponse,
): Promise<void> {
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<void> {
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<void> {
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 body = await this.parseRequestBody(req);
if (!body) {
this.sendError(res, 400, 'Invalid JSON body', 'invalid_request_error');
return;
}
await this.embeddingsHandler.handleEmbeddings(res, body);
}
/**
* Parse request body
*/
private async parseRequestBody(req: http.IncomingMessage): Promise<unknown | null> {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
// Limit body size
if (body.length > 10 * 1024 * 1024) {
resolve(null);
}
});
req.on('end', () => {
try {
resolve(JSON.parse(body));
} catch {
resolve(null);
}
});
req.on('error', () => {
resolve(null);
});
});
}
/**
* Convert legacy completion request to chat format
*/
private convertCompletionToChat(body: Record<string, unknown>): Record<string, unknown> {
const prompt = body.prompt as string | string[];
const promptText = Array.isArray(prompt) ? prompt.join('\n') : prompt;
return {
model: body.model,
messages: [
{ role: 'user', content: promptText },
],
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
n: body.n,
stream: body.stream,
stop: body.stop,
};
}
/**
* Send error response
*/
private sendError(
res: http.ServerResponse,
statusCode: number,
message: string,
type: string,
param?: string,
): void {
const error: IApiError = {
error: {
message,
type,
param,
code: null,
},
};
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(error));
}
}