import express, { Request, Response, NextFunction } from 'express';
import { env } from '../config/env.js';
import { logger } from '../logging/logger.js';
import { metrics } from '../logging/metrics.js';
import { contextManager } from '../context/manager.js';
import { routeRequest, detectComplexity } from '../routing/router.js';
import { db } from '../db/postgres.js';
import { redisCache } from '../cache/redis.js';
import { providerHealth } from '../config/provider-health.js';
import { SemanticSearch, KnowledgePackManager } from '../search/semantic.js';
import { modelConfigService } from '../db/model-config.js';
import { gptPlusClient } from '../tools/llm/gpt-plus.js';
import { terminalManager } from '../tools/terminal/index.js';
import { TerminalConnectionService } from '../db/terminal-connections.js';
import { authService } from '../db/auth.js';
import type { TaskType } from '../mcp/types.js';
import { createDatabaseRoutes } from './database.js';
import { createProviderRoutes } from './providers.js';
import { createOpenRouterRoutes } from './openrouter.js';
import { createAgentRoutes } from './agents.js';
import { createAdminRoutes } from './admin.js';
import configRoutes from './routes/config.js';
import deploymentsRoutes from './routes/deployments.js';
import {
buildContextForRequest,
queueMessageEmbedding,
saveAssistantResponse,
type ChatContextStrategy
} from '../services/chat/index.js';
/**
* HTTP API Server for stateless request handling
*/
import { Server } from 'http';
export class APIServer {
private app: express.Application;
private server: Server | null = null;
private routesReady: Promise<void>;
constructor() {
this.app = express();
this.setupMiddleware();
this.routesReady = this.setupRoutes();
}
/**
* Setup Express middleware
*/
private setupMiddleware() {
// CORS
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.header('Access-Control-Allow-Origin', env.API_CORS_ORIGIN);
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
);
if (req.method === 'OPTIONS') {
res.sendStatus(200);
return;
}
next();
});
// JSON parsing
this.app.use(express.json({ limit: '10mb' }));
// Request logging and metrics
this.app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
// Skip OPTIONS, health checks, and internal endpoints from metrics
const skipPaths = ['/health', '/metrics', '/v1/models/layers', '/v1/providers'];
const shouldSkipMetrics = req.method === 'OPTIONS' ||
skipPaths.some(path => req.path.startsWith(path));
if (!shouldSkipMetrics) {
metrics.recordRequest();
}
logger.info('API request', {
method: req.method,
path: req.path,
conversationId: req.body?.conversationId,
});
next();
});
}
private authServiceInitialized = false;
/**
* Ensure auth service is initialized with database pool
* This is called lazily to handle the case where database isn't ready at startup
*/
private async ensureAuthServiceInit(): Promise<boolean> {
if (this.authServiceInitialized) {
return true;
}
if (db.isReady()) {
const pool = db.getPool();
if (pool) {
try {
authService.init(pool);
await authService.ensureTable();
this.authServiceInitialized = true;
logger.info('Auth service initialized successfully');
return true;
} catch (error) {
logger.error('Failed to initialize auth service', {
error: error instanceof Error ? error.message : 'Unknown',
});
}
}
}
return false;
}
/**
* Setup API routes
*/
private async setupRoutes() {
// Try to initialize auth service with database pool (may fail if db not ready yet)
await this.ensureAuthServiceInit();
// ==================
// Auth Routes (Public - no middleware)
// ==================
// Login
this.app.post('/v1/auth/login', async (req, res) => {
await this.handleLogin(req, res);
});
// Check auth status
this.app.get('/v1/auth/status', (req, res) => {
res.json({
authEnabled: authService.isAuthEnabled(),
message: authService.isAuthEnabled()
? 'Authentication is enabled. Use /v1/auth/login to authenticate.'
: 'Authentication is disabled. All routes are publicly accessible.',
});
});
// Verify token
this.app.post('/v1/auth/verify', async (req, res) => {
await this.handleVerifyToken(req, res);
});
// ==================
// Auth Middleware (applies to routes below if enabled)
// ==================
this.app.use('/v1', this.authMiddleware.bind(this));
// Database management routes (use factory function that works with bundled code)
this.app.use('/v1/database', createDatabaseRoutes());
this.app.use('/v1/redis', createDatabaseRoutes());
// Provider management routes (use factory function that works with bundled code)
this.app.use('/v1/providers', createProviderRoutes());
// OpenRouter info routes (use factory function that works with bundled code)
this.app.use('/v1/openrouter', createOpenRouterRoutes());
// AI Agent routes
this.app.use('/v1/agents', createAgentRoutes());
// Admin routes for MCP tools settings and backend configurations
this.app.use('/v1/admin', createAdminRoutes());
// Configuration management routes (database-backed config)
this.app.use('/v1/config', configRoutes);
// Deployment workflow routes (command generation, sessions, confirmations)
this.app.use('/v1/deployments', deploymentsRoutes);
// MikroTik Command Builder routes (facts, plan, compile, validate, apply)
const mikrotikRoutes = (await import('./routes/mikrotik.js')).default;
this.app.use('/v1/mikrotik', mikrotikRoutes);
// Health check
this.app.get('/health', async (_req, res) => {
// Get provider health status
const providers = await providerHealth.getHealthyProviders();
const providerStatus = providerHealth.getProviderStatusSummary();
// Import models config
const { getModelsByLayer, LAYERS_IN_ORDER } = await import('../config/models.js');
// Build layers status
const layersStatus: Record<string, {
enabled: boolean;
models: string[];
providers: string[];
}> = {};
for (const layer of LAYERS_IN_ORDER) {
const models = getModelsByLayer(layer);
const layerProviders = new Set(models.map(m => m.provider));
const healthyProviders = Array.from(layerProviders).filter(p => providers.includes(p));
layersStatus[layer] = {
enabled: models.length > 0 && healthyProviders.length > 0,
models: models.map(m => m.id),
providers: healthyProviders,
};
}
res.json({
status: 'ok',
redis: redisCache.isReady(),
database: db.isReady(),
timestamp: new Date().toISOString(),
providers: providerStatus,
layers: layersStatus,
healthyProviders: providers,
configuration: {
logLevel: env.LOG_LEVEL,
defaultLayer: env.DEFAULT_LAYER,
enableCrossCheck: env.ENABLE_CROSS_CHECK,
enableAutoEscalate: env.ENABLE_AUTO_ESCALATE,
maxEscalationLayer: env.MAX_ESCALATION_LAYER,
enableCostTracking: env.ENABLE_COST_TRACKING,
costAlertThreshold: env.COST_ALERT_THRESHOLD,
layerControl: {
L0: env.LAYER_L0_ENABLED,
L1: env.LAYER_L1_ENABLED,
L2: env.LAYER_L2_ENABLED,
L3: env.LAYER_L3_ENABLED,
},
taskSpecificModels: {
chat: env.CHAT_MODELS || 'default',
code: env.CODE_MODELS || 'default',
analyze: env.ANALYZE_MODELS || 'default',
createProject: env.CREATE_PROJECT_MODELS || 'default',
},
},
});
});
// Route request (intelligent model selection)
this.app.post('/v1/route', async (req, res) => {
await this.handleRoute(req, res);
});
// Code agent endpoint
this.app.post('/v1/code-agent', async (req, res) => {
await this.handleCodeAgent(req, res);
});
// Chat endpoint (general purpose)
this.app.post('/v1/chat', async (req, res) => {
await this.handleChat(req, res);
});
// OpenAI-compatible chat completions endpoint (for dashboard chat)
this.app.post('/v1/chat/completions', async (req, res) => {
await this.handleChatCompletions(req, res);
});
// Context endpoints
this.app.get('/v1/context/:conversationId', async (req, res) => {
await this.handleGetContext(req, res);
});
this.app.post('/v1/context/:conversationId', async (req, res) => {
await this.handleUpdateContext(req, res);
});
// Cache management
this.app.post('/v1/cache/clear', async (req, res) => {
await this.handleCacheClear(req, res);
});
// Stats endpoints
this.app.get('/v1/stats', async (req, res) => {
await this.handleGetStats(req, res);
});
this.app.get('/v1/stats/conversation/:conversationId', async (req, res) => {
await this.handleGetConversationStats(req, res);
});
// Server stats (real-time metrics from memory)
this.app.get('/v1/server-stats', (_req, res) => {
this.handleGetServerStats(res);
});
// MCP CLI endpoint (for CLI tool)
this.app.post('/v1/mcp-cli', async (req, res) => {
await this.handleMCPCLI(req, res);
});
// Analytics endpoints (Phase 1)
this.app.get('/v1/analytics', async (req, res) => {
await this.handleGetAnalytics(req, res);
});
this.app.get('/v1/analytics/top-expensive', async (req, res) => {
await this.handleGetTopExpensive(req, res);
});
this.app.get('/v1/analytics/error-rate', async (req, res) => {
await this.handleGetErrorRate(req, res);
});
// Quota endpoints (Phase 1)
this.app.get('/v1/quota/status', async (req, res) => {
await this.handleGetQuotaStatus(req, res);
});
this.app.post('/v1/quota/update', async (req, res) => {
await this.handleUpdateQuota(req, res);
});
// Tracing endpoints (Phase 1)
this.app.get('/v1/traces/:traceId', async (req, res) => {
await this.handleGetTrace(req, res);
});
// Semantic search endpoints (Phase 4)
this.app.post('/v1/search/code', async (req, res) => {
await this.handleSemanticSearch(req, res);
});
this.app.post('/v1/search/index', async (req, res) => {
await this.handleIndexCode(req, res);
});
this.app.get('/v1/search/stats', async (req, res) => {
await this.handleSearchStats(req, res);
});
// Knowledge pack endpoints (Phase 4)
this.app.post('/v1/knowledge/pack', async (req, res) => {
await this.handleCreateKnowledgePack(req, res);
});
this.app.get('/v1/knowledge/pack/:packId', async (req, res) => {
await this.handleLoadKnowledgePack(req, res);
});
this.app.get('/v1/knowledge/search', async (req, res) => {
await this.handleSearchKnowledgePacks(req, res);
});
// Model management endpoints
this.app.get('/v1/models', async (req, res) => {
await this.handleGetModels(req, res);
});
this.app.get('/v1/models/layers', async (req, res) => {
await this.handleGetLayers(req, res);
});
this.app.put('/v1/models/:modelId', async (req, res) => {
await this.handleUpdateModel(req, res);
});
this.app.post('/v1/models', async (req, res) => {
await this.handleAddModel(req, res);
});
this.app.delete('/v1/models/:modelId', async (req, res) => {
await this.handleDeleteModel(req, res);
});
// Reorder models in a layer
this.app.put('/v1/layers/:layerId/reorder', async (req, res) => {
await this.handleReorderModels(req, res);
});
this.app.put('/v1/layers/:layerId/toggle', async (req, res) => {
await this.handleToggleLayer(req, res);
});
// Provider management endpoints
this.app.get('/v1/providers', async (req, res) => {
await this.handleGetProviders(req, res);
});
this.app.post('/v1/providers', async (req, res) => {
await this.handleAddProvider(req, res);
});
this.app.put('/v1/providers/:providerId', async (req, res) => {
await this.handleUpdateProvider(req, res);
});
this.app.delete('/v1/providers/:providerId', async (req, res) => {
await this.handleDeleteProvider(req, res);
});
// GPT Plus endpoints
this.app.get('/v1/gpt-plus/status', async (req, res) => {
await this.handleGPTPlusStatus(req, res);
});
this.app.post('/v1/gpt-plus/login', async (req, res) => {
await this.handleGPTPlusLogin(req, res);
});
this.app.post('/v1/gpt-plus/logout', async (req, res) => {
await this.handleGPTPlusLogout(req, res);
});
this.app.get('/v1/gpt-plus/models', async (req, res) => {
await this.handleGPTPlusModels(req, res);
});
this.app.post('/v1/gpt-plus/chat', async (req, res) => {
await this.handleGPTPlusChat(req, res);
});
// Terminal/CLI endpoints
this.app.get('/v1/terminal/sessions', async (req, res) => {
await this.handleGetTerminalSessions(req, res);
});
this.app.post('/v1/terminal/local', async (req, res) => {
await this.handleCreateLocalSession(req, res);
});
this.app.post('/v1/terminal/ssh', async (req, res) => {
await this.handleCreateSSHSession(req, res);
});
this.app.post('/v1/terminal/telnet', async (req, res) => {
await this.handleCreateTelnetSession(req, res);
});
this.app.post('/v1/terminal/:sessionId/execute', async (req, res) => {
await this.handleExecuteCommand(req, res);
});
this.app.post('/v1/terminal/:sessionId/send', async (req, res) => {
await this.handleSendData(req, res);
});
this.app.get('/v1/terminal/:sessionId/output', async (req, res) => {
await this.handleGetSessionOutput(req, res);
});
this.app.delete('/v1/terminal/:sessionId', async (req, res) => {
await this.handleCloseSession(req, res);
});
// Terminal connection profiles (saved connections)
this.app.get('/v1/terminal/connections', async (req, res) => {
await this.handleGetTerminalConnections(req, res);
});
this.app.post('/v1/terminal/connections', async (req, res) => {
await this.handleCreateTerminalConnection(req, res);
});
this.app.get('/v1/terminal/connections/:connectionId', async (req, res) => {
await this.handleGetTerminalConnectionById(req, res);
});
this.app.put('/v1/terminal/connections/:connectionId', async (req, res) => {
await this.handleUpdateTerminalConnection(req, res);
});
this.app.delete('/v1/terminal/connections/:connectionId', async (req, res) => {
await this.handleDeleteTerminalConnection(req, res);
});
this.app.post('/v1/terminal/connections/:connectionId/connect', async (req, res) => {
await this.handleConnectFromProfile(req, res);
});
// 404 handler
this.app.use((req, res) => {
res.status(404).json({
error: 'Not found',
path: req.path,
});
});
}
/**
* Check quota before processing request
*/
private async checkQuotaForRequest(
userId: string,
projectId: string,
estimatedTokens: number,
estimatedCost: number,
): Promise<{ allowed: boolean; error?: { status: number; body: unknown } }> {
if (!db.isReady()) {
// Skip quota check if database unavailable
return { allowed: true };
}
try {
const { QuotaEnforcer } = await import('../quota/enforcer.js');
const enforcer = new QuotaEnforcer(db.getPool());
const quotaCheck = await enforcer.checkQuota(
userId || 'anonymous',
projectId || 'default-project',
estimatedTokens,
estimatedCost,
);
if (!quotaCheck.allowed) {
return {
allowed: false,
error: {
status: 429,
body: {
error: 'Quota exceeded',
details: quotaCheck.reason,
remaining: quotaCheck.remaining,
resetAt: quotaCheck.resetAt,
},
},
};
}
return { allowed: true };
} catch (error) {
logger.warn('Quota check failed, allowing request', {
error: error instanceof Error ? error.message : 'Unknown',
});
return { allowed: true };
}
}
/**
* Increment quota after successful request
*/
private async incrementQuotaAfterRequest(
userId: string,
projectId: string,
tokens: number,
cost: number,
): Promise<void> {
if (!db.isReady()) return;
try {
const { QuotaEnforcer } = await import('../quota/enforcer.js');
const enforcer = new QuotaEnforcer(db.getPool());
await enforcer.incrementQuota(
userId || 'anonymous',
projectId || 'default-project',
tokens,
cost,
);
} catch (error) {
logger.error('Failed to increment quota', {
error: error instanceof Error ? error.message : 'Unknown',
});
}
}
/**
* Handle /v1/route endpoint
*/
private async handleRoute(req: Request, res: Response): Promise<void> {
const startTime = Date.now();
try {
const {
conversationId,
message,
userId,
projectId,
qualityLevel,
} = req.body;
if (!conversationId || !message) {
res.status(400).json({
error: 'Missing required fields: conversationId, message',
});
return;
}
// Ensure conversation exists
await contextManager.ensureConversation(
conversationId,
userId,
projectId
);
// Ensure conversation context is loaded
await contextManager.getSummary(conversationId);
// Check quota before routing (estimate: 1000 tokens, $0.01)
const quotaCheck = await this.checkQuotaForRequest(
userId,
projectId,
1000,
0.01,
);
if (!quotaCheck.allowed && quotaCheck.error) {
res.status(quotaCheck.error.status).json(quotaCheck.error.body);
return;
}
// Route request
const result = await routeRequest(
{ prompt: message },
{
quality: qualityLevel || 'normal',
complexity: 'medium',
taskType: 'general',
}
);
// Save message to context
await contextManager.addMessage(conversationId, {
role: 'user',
content: message,
});
await contextManager.addMessage(conversationId, {
role: 'assistant',
content: result.content,
metadata: {
modelUsed: result.modelId,
provider: result.provider,
},
});
// Log to DB
await db.insert('llm_calls', {
conversation_id: conversationId,
model_id: result.modelId,
layer: 'L0',
input_tokens: result.inputTokens,
output_tokens: result.outputTokens,
estimated_cost: result.cost,
duration_ms: Date.now() - startTime,
success: true,
});
// Increment quota after successful request
await this.incrementQuotaAfterRequest(
userId,
projectId,
result.inputTokens + result.outputTokens,
result.cost,
);
res.json({
result: {
response: result.content,
model: result.modelId,
provider: result.provider,
},
routing: {
summary: result.routingSummary,
fromCache: false,
},
context: {
conversationId,
},
performance: {
durationMs: Date.now() - startTime,
tokens: {
input: result.inputTokens,
output: result.outputTokens,
},
cost: result.cost,
},
});
} catch (error) {
logger.error('Route request error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
message:
error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle /v1/code-agent endpoint
*/
private async handleCodeAgent(req: Request, res: Response): Promise<void> {
const startTime = Date.now();
try {
const { conversationId, task, files, userId, projectId } = req.body;
if (!conversationId || !task) {
res.status(400).json({
error: 'Missing required fields: conversationId, task',
});
return;
}
// Ensure conversation exists
await contextManager.ensureConversation(
conversationId,
userId,
projectId
);
// Get context
const summary = await contextManager.getSummary(conversationId);
// Check quota before routing (code tasks estimate: 2000 tokens, $0.02)
const quotaCheck = await this.checkQuotaForRequest(
userId,
projectId,
2000,
0.02,
);
if (!quotaCheck.allowed && quotaCheck.error) {
res.status(quotaCheck.error.status).json(quotaCheck.error.body);
return;
}
// Build prompt for code agent
const prompt = `Task: ${task}\n\nFiles: ${files ? JSON.stringify(files) : 'N/A'}\n\nContext: ${summary ? JSON.stringify(summary) : 'New conversation'}`;
// Route request
const result = await routeRequest(
{ prompt },
{
quality: 'high',
complexity: 'high',
taskType: 'code',
}
);
// Save to context
await contextManager.addMessage(conversationId, {
role: 'user',
content: task,
metadata: { type: 'code-agent', files },
});
await contextManager.addMessage(conversationId, {
role: 'assistant',
content: result.content,
metadata: {
type: 'code-agent',
modelUsed: result.modelId,
},
});
// Increment quota after successful request
await this.incrementQuotaAfterRequest(
userId,
projectId,
result.inputTokens + result.outputTokens,
result.cost,
);
res.json({
result: {
response: result.content,
model: result.modelId,
provider: result.provider,
},
performance: {
durationMs: Date.now() - startTime,
cost: result.cost,
},
});
} catch (error) {
logger.error('Code agent error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
message:
error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle /v1/chat endpoint
*/
private async handleChat(req: Request, res: Response): Promise<void> {
const startTime = Date.now();
try {
const { conversationId, message, userId, projectId } = req.body;
if (!conversationId || !message) {
res.status(400).json({
error: 'Missing required fields: conversationId, message',
});
return;
}
// Ensure conversation exists
await contextManager.ensureConversation(
conversationId,
userId,
projectId
);
// Get context
const summary = await contextManager.getSummary(conversationId);
const recentMessages =
await contextManager.getRecentMessages(conversationId);
// Build context-aware prompt
const contextStr = summary
? `Context: ${JSON.stringify(summary)}\n\nRecent messages: ${JSON.stringify(recentMessages)}`
: '';
const fullPrompt = contextStr
? `${contextStr}\n\nUser: ${message}`
: message;
// Check quota before routing (chat estimate: 1500 tokens, $0.015)
const quotaCheck = await this.checkQuotaForRequest(
userId,
projectId,
1500,
0.015,
);
if (!quotaCheck.allowed && quotaCheck.error) {
res.status(quotaCheck.error.status).json(quotaCheck.error.body);
return;
}
// Route request
const result = await routeRequest(
{ prompt: fullPrompt },
{
quality: 'normal',
complexity: 'medium',
taskType: 'general',
}
);
// Save messages
await contextManager.addMessage(conversationId, {
role: 'user',
content: message,
});
await contextManager.addMessage(conversationId, {
role: 'assistant',
content: result.content,
});
// Increment quota after successful request
await this.incrementQuotaAfterRequest(
userId,
projectId,
result.inputTokens + result.outputTokens,
result.cost,
);
res.json({
result: {
response: result.content,
model: result.modelId,
},
performance: {
durationMs: Date.now() - startTime,
cost: result.cost,
},
});
} catch (error) {
logger.error('Chat error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
message:
error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle /v1/chat/completions - OpenAI-compatible chat completions
* Used by the dashboard chat interface
*
* Enhanced with Chat Context Optimization for long conversations
*/
private async handleChatCompletions(req: Request, res: Response): Promise<void> {
const startTime = Date.now();
try {
const {
messages,
model,
layer,
temperature,
max_tokens,
// New context optimization parameters
conversation_id,
context_strategy,
max_context_tokens,
project_id,
tool_id,
} = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
res.status(400).json({
error: 'Missing required field: messages (array)',
});
return;
}
const lastUserMessage = messages.filter((m: any) => m.role === 'user').pop();
if (!lastUserMessage) {
res.status(400).json({
error: 'At least one user message is required',
});
return;
}
// Build optimized context using ChatContextBuilder
let contextResult;
let fullPrompt: string;
let tokensSaved = 0;
try {
contextResult = await buildContextForRequest({
conversationId: conversation_id,
messages: messages,
model: model,
layer: layer,
projectId: project_id,
toolId: tool_id,
contextStrategy: context_strategy as ChatContextStrategy,
maxContextTokens: max_context_tokens,
});
fullPrompt = contextResult.prompt;
tokensSaved = contextResult.tokenStats.saved;
logger.info('Chat context optimized', {
conversationId: conversation_id,
strategy: contextResult.strategy,
originalTokens: contextResult.tokenStats.total + tokensSaved,
optimizedTokens: contextResult.tokenStats.total,
tokensSaved,
spansRetrieved: contextResult.metadata.spansRetrieved,
summaryIncluded: contextResult.metadata.summaryIncluded,
});
} catch (contextError) {
// Fallback to legacy prompt building if context optimization fails
logger.warn('Context optimization failed, using legacy prompt building', {
error: contextError instanceof Error ? contextError.message : 'Unknown',
});
const systemMessages = messages.filter((m: any) => m.role === 'system');
const userMessages = messages.filter((m: any) => m.role !== 'system');
const systemPrompt = systemMessages.length > 0
? systemMessages.map((m: any) => m.content).join('\n')
: '';
const conversationHistory = userMessages.slice(0, -1)
.map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
.join('\n');
fullPrompt = [
systemPrompt ? `System: ${systemPrompt}` : '',
conversationHistory,
`User: ${lastUserMessage.content}`
].filter(Boolean).join('\n\n');
}
// Determine routing options
const routingOptions: any = {
quality: 'normal',
complexity: 'medium',
taskType: 'general' as const,
};
// If specific layer requested
if (layer && ['L0', 'L1', 'L2', 'L3'].includes(layer)) {
routingOptions.preferredLayer = layer;
}
// If specific model requested
if (model && model !== 'auto') {
routingOptions.preferredModel = model;
}
// Route request
const result = await routeRequest(
{
prompt: fullPrompt,
maxTokens: max_tokens || 4096,
temperature: temperature ?? 0.7,
},
routingOptions
);
const latency = Date.now() - startTime;
// Queue embedding generation for the new messages (non-blocking)
if (conversation_id) {
queueMessageEmbedding(conversation_id, lastUserMessage.content);
if (result.content) {
// Save assistant response to database
await saveAssistantResponse(conversation_id, result.content, result.modelId);
}
}
// Return OpenAI-compatible response with context optimization stats
res.json({
id: `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: result.modelId,
layer: result.layer,
content: result.content,
message: result.content,
usage: {
prompt_tokens: result.inputTokens,
completion_tokens: result.outputTokens,
total_tokens: result.inputTokens + result.outputTokens,
},
cost: result.cost,
latency,
// Context optimization metadata
context_optimization: contextResult ? {
strategy: contextResult.strategy,
tokens_saved: tokensSaved,
summary_included: contextResult.metadata.summaryIncluded,
spans_retrieved: contextResult.metadata.spansRetrieved,
recent_messages_included: contextResult.metadata.recentMessagesIncluded,
} : null,
choices: [{
index: 0,
message: {
role: 'assistant',
content: result.content,
},
finish_reason: 'stop',
}],
});
} catch (error) {
logger.error('Chat completions error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Chat completion failed',
message: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/context/:conversationId
*/
private async handleGetContext(req: Request, res: Response) {
try {
const { conversationId } = req.params;
const summary = await contextManager.getSummary(conversationId);
const messages =
await contextManager.getRecentMessages(conversationId);
res.json({
conversationId,
summary,
recentMessages: messages,
});
} catch (error) {
logger.error('Get context error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Handle POST /v1/context/:conversationId
*/
private async handleUpdateContext(req: Request, res: Response): Promise<void> {
try {
const { conversationId } = req.params;
const { summary } = req.body;
if (!summary) {
res.status(400).json({
error: 'Missing required field: summary',
});
return;
}
await contextManager.updateSummary(conversationId, {
...summary,
conversationId,
});
res.json({
success: true,
conversationId,
});
} catch (error) {
logger.error('Update context error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Handle POST /v1/cache/clear
*/
private async handleCacheClear(req: Request, res: Response): Promise<void> {
try {
const { pattern, conversationId } = req.body;
let clearedCount = 0;
if (conversationId) {
// Clear specific conversation cache
await contextManager.clearCache(conversationId);
clearedCount = 1;
logger.info('Cleared conversation cache', { conversationId });
} else if (pattern) {
// Clear by pattern
clearedCount = await redisCache.deleteByPattern(pattern);
logger.info('Cleared cache by pattern', { pattern, count: clearedCount });
} else {
res.status(400).json({
error: 'Must provide either conversationId or pattern',
});
return;
}
res.json({
success: true,
clearedCount,
pattern: pattern || `conversation:${conversationId}`,
});
} catch (error) {
logger.error('Cache clear error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Handle GET /v1/stats
*/
private async handleGetStats(req: Request, res: Response): Promise<void> {
try {
const { userId, startDate, endDate, groupBy } = req.query;
// Build query based on filters
let whereClause = 'WHERE 1=1';
const params: unknown[] = [];
if (userId) {
params.push(userId);
whereClause += ` AND c.user_id = $${params.length}`;
}
if (startDate) {
params.push(startDate);
whereClause += ` AND l.created_at >= $${params.length}`;
}
if (endDate) {
params.push(endDate);
whereClause += ` AND l.created_at <= $${params.length}`;
}
// Get overall stats
const statsQuery = `
SELECT
COUNT(*) as total_calls,
SUM(estimated_cost) as total_cost,
SUM(input_tokens) as total_input_tokens,
SUM(output_tokens) as total_output_tokens,
SUM(CASE WHEN cached THEN 1 ELSE 0 END) as cache_hits
FROM llm_calls l
LEFT JOIN conversations c ON l.conversation_id = c.id
${whereClause}
`;
const statsResult = await db.query<{
total_calls: string;
total_cost: string;
total_input_tokens: string;
total_output_tokens: string;
cache_hits: string;
}>(statsQuery, params);
const stats = statsResult?.rows[0] || {
total_calls: '0',
total_cost: '0',
total_input_tokens: '0',
total_output_tokens: '0',
cache_hits: '0',
};
const response: {
totalCalls: number;
totalCost: number;
totalTokens: { input: number; output: number };
cacheHitRate: number;
byModel?: unknown;
byLayer?: unknown;
} = {
totalCalls: parseInt(stats.total_calls || '0'),
totalCost: parseFloat(stats.total_cost || '0'),
totalTokens: {
input: parseInt(stats.total_input_tokens || '0'),
output: parseInt(stats.total_output_tokens || '0'),
},
cacheHitRate: stats.total_calls
? parseInt(stats.cache_hits || '0') / parseInt(stats.total_calls)
: 0,
};
// Group by if requested
if (groupBy === 'model') {
const modelStatsQuery = `
SELECT
model_id,
COUNT(*) as calls,
SUM(estimated_cost) as cost,
SUM(input_tokens) as input_tokens,
SUM(output_tokens) as output_tokens
FROM llm_calls l
LEFT JOIN conversations c ON l.conversation_id = c.id
${whereClause}
GROUP BY model_id
`;
const modelStatsResult = await db.query<{
model_id: string;
calls: string;
cost: string;
input_tokens: string;
output_tokens: string;
}>(modelStatsQuery, params);
response.byModel = {};
modelStatsResult?.rows.forEach((row: {
model_id: string;
calls: string;
cost: string;
input_tokens: string;
output_tokens: string;
}) => {
(response.byModel as Record<string, unknown>)[row.model_id] = {
calls: parseInt(row.calls),
cost: parseFloat(row.cost),
tokens: {
input: parseInt(row.input_tokens),
output: parseInt(row.output_tokens),
},
};
});
} else if (groupBy === 'layer') {
const layerStatsQuery = `
SELECT
layer,
COUNT(*) as calls,
SUM(estimated_cost) as cost
FROM llm_calls l
LEFT JOIN conversations c ON l.conversation_id = c.id
${whereClause}
GROUP BY layer
`;
const layerStatsResult = await db.query<{
layer: string;
calls: string;
cost: string;
}>(layerStatsQuery, params);
response.byLayer = {};
layerStatsResult?.rows.forEach((row: {
layer: string;
calls: string;
cost: string;
}) => {
(response.byLayer as Record<string, unknown>)[row.layer] = {
calls: parseInt(row.calls),
cost: parseFloat(row.cost),
};
});
}
res.json(response);
} catch (error) {
logger.error('Get stats error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Handle GET /v1/stats/conversation/:conversationId
*/
private async handleGetConversationStats(req: Request, res: Response): Promise<void> {
try {
const { conversationId } = req.params;
const statsQuery = `
SELECT
COUNT(DISTINCT m.id) as message_count,
COUNT(DISTINCT l.id) as llm_calls,
SUM(l.estimated_cost) as total_cost,
SUM(l.input_tokens) as input_tokens,
SUM(l.output_tokens) as output_tokens,
c.created_at,
c.updated_at
FROM conversations c
LEFT JOIN messages m ON c.id = m.conversation_id
LEFT JOIN llm_calls l ON c.id = l.conversation_id
WHERE c.id = $1
GROUP BY c.id, c.created_at, c.updated_at
`;
const result = await db.query<{
message_count: string;
llm_calls: string;
total_cost: string;
input_tokens: string;
output_tokens: string;
created_at: Date;
updated_at: Date;
}>(statsQuery, [conversationId]);
if (!result || result.rows.length === 0) {
res.status(404).json({
error: 'Conversation not found',
});
return;
}
const stats = result.rows[0];
res.json({
conversationId,
messageCount: parseInt(stats.message_count || '0'),
llmCalls: parseInt(stats.llm_calls || '0'),
totalCost: parseFloat(stats.total_cost || '0'),
totalTokens: {
input: parseInt(stats.input_tokens || '0'),
output: parseInt(stats.output_tokens || '0'),
},
createdAt: stats.created_at,
updatedAt: stats.updated_at,
});
} catch (error) {
logger.error('Get conversation stats error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Handle GET /v1/server-stats - Real-time server metrics
*/
private handleGetServerStats(res: Response): void {
try {
const metricsData = metrics.getMetrics();
const uptime = process.uptime();
const memoryUsage = process.memoryUsage();
res.json({
uptime: {
seconds: Math.floor(uptime),
formatted: this.formatUptime(uptime),
},
requests: {
total: metricsData.totalRequests,
averageDuration: metricsData.averageDuration,
},
llm: {
totalCalls: metricsData.totalLLMCalls,
tokens: {
input: metricsData.totalInputTokens,
output: metricsData.totalOutputTokens,
total: metricsData.totalTokens,
},
cost: {
total: metricsData.totalCost,
currency: 'USD',
},
},
memory: {
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
rss: Math.round(memoryUsage.rss / 1024 / 1024),
external: Math.round(memoryUsage.external / 1024 / 1024),
unit: 'MB',
},
providers: {
openai: providerHealth.isProviderHealthy('openai'),
anthropic: providerHealth.isProviderHealthy('anthropic'),
openrouter: providerHealth.isProviderHealthy('openrouter'),
ossLocal: providerHealth.isProviderHealthy('oss-local'),
},
cache: {
redis: redisCache.isReady(),
},
database: {
postgres: db.isReady(),
},
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error('Get server stats error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
});
}
}
/**
* Format uptime in human readable format
*/
private formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
}
/**
* Handle POST /v1/mcp-cli - MCP CLI tool requests
*/
private async handleMCPCLI(req: Request, res: Response): Promise<void> {
try {
const { mode, message, context, budget } = req.body;
if (!mode || !message) {
res.status(400).json({
error: 'Missing required fields: mode, message',
});
return;
}
// Validate mode
if (!['chat', 'code', 'diff'].includes(mode)) {
res.status(400).json({
error: 'Invalid mode. Must be: chat, code, or diff',
});
return;
}
// Build system prompt based on mode
let systemPrompt = '';
let taskType: TaskType = 'general';
let complexity: 'low' | 'medium' | 'high' = 'medium';
let preferredLayer: 'L0' | 'L1' | 'L2' | 'L3' | undefined;
// Check if user explicitly requested a layer (e.g., "use L0", "with layer L2")
const layerMatch = message.match(/(?:use|with|on|at)\s+(?:layer\s+)?(L[0-3])/i);
if (layerMatch) {
preferredLayer = layerMatch[1].toUpperCase() as 'L0' | 'L1' | 'L2' | 'L3';
logger.info('User requested specific layer', {
layer: preferredLayer,
originalMessage: message.substring(0, 50)
});
}
switch (mode) {
case 'chat':
systemPrompt = 'You are a helpful AI assistant. Provide clear, concise answers.';
taskType = 'general';
// Only detect complexity if no layer specified
if (!preferredLayer) {
complexity = await detectComplexity(message);
}
break;
case 'code':
systemPrompt = `You are an expert code reviewer and analyzer.
Analyze the provided code and give detailed feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance optimizations
- Security concerns
- Suggestions for improvement
${context?.language ? `Language: ${context.language}` : ''}
${context?.filename ? `File: ${context.filename}` : ''}`;
taskType = 'code';
complexity = 'high';
break;
case 'diff':
systemPrompt = `You are an expert code editor. Generate a unified diff patch that applies the requested changes.
IMPORTANT: Your response must ONLY be a valid unified diff in this exact format:
\`\`\`diff
--- a/path/to/file
+++ b/path/to/file
@@ -start,count +start,count @@
context line
-removed line
+added line
context line
\`\`\`
Do not include any explanations, comments, or additional text outside the diff block.
Include at least 3 lines of context before and after changes.
${context?.filename ? `File: ${context.filename}` : ''}
${context?.language ? `Language: ${context.language}` : ''}`;
taskType = 'code';
complexity = 'high';
break;
}
// Construct full prompt with context
let fullPrompt = message;
if (context) {
const contextParts: string[] = [];
if (context.cwd) contextParts.push(`Current directory: ${context.cwd}`);
if (context.files && context.files.length > 0) {
contextParts.push(`Files in directory:\n${context.files.slice(0, 20).join('\n')}`);
}
if (context.gitStatus) contextParts.push(`Git status:\n${context.gitStatus}`);
if (contextParts.length > 0) {
fullPrompt = `${contextParts.join('\n\n')}\n\n${message}`;
}
}
// Route request through the gateway with system prompt
// Use 'normal' quality for chat mode to prefer L0 free models
// Use 'high' quality for code/diff modes for better accuracy
const quality = mode === 'chat' ? 'normal' : 'high';
const result = await routeRequest(
{
prompt: fullPrompt,
systemPrompt,
},
{
quality,
complexity,
taskType,
preferredLayer, // Pass preferred layer if user specified
budget: typeof budget === 'number' ? budget : undefined, // Pass budget if provided
}
);
// Determine which layer was used (from routing summary or preferred layer)
const routingLayerMatch = result.routingSummary?.match(/layer ([A-Z]\d)/);
const usedLayer = preferredLayer || (routingLayerMatch ? routingLayerMatch[1] : env.DEFAULT_LAYER);
// Format response based on mode
let responseMessage = result.content;
let patch: string | undefined;
if (mode === 'diff') {
// Extract diff from code blocks if present
const diffMatch = result.content.match(/```diff\n([\s\S]+?)\n```/);
if (diffMatch) {
patch = diffMatch[1];
responseMessage = 'Diff generated successfully';
} else {
// If no code block, assume entire response is the diff
patch = result.content;
responseMessage = 'Diff generated successfully';
}
}
res.json({
message: responseMessage,
patch,
model: result.modelId,
tokens: {
input: result.inputTokens || 0,
output: result.outputTokens || 0,
total: (result.inputTokens || 0) + (result.outputTokens || 0),
},
cost: result.cost || 0,
metadata: {
complexity,
layer: usedLayer,
model: result.modelId,
tokens: {
input: result.inputTokens || 0,
output: result.outputTokens || 0,
total: (result.inputTokens || 0) + (result.outputTokens || 0),
},
cost: result.cost || 0,
},
escalation: result.requiresEscalationConfirm ? {
required: true,
currentLayer: usedLayer,
suggestedLayer: result.suggestedLayer,
reason: result.escalationReason,
message: '⚠️ The current layer detected conflicts. A higher tier (paid) layer is suggested for better results. Would you like to escalate?',
optimizedPrompt: result.optimizedPrompt,
} : undefined,
});
} catch (error) {
logger.error('MCP CLI error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/analytics
*/
private async handleGetAnalytics(req: Request, res: Response): Promise<void> {
try {
const { AnalyticsAggregator } = await import('../analytics/aggregator.js');
const aggregator = new AnalyticsAggregator(db.getPool());
const {
projectId,
userId,
startDate,
endDate,
groupBy,
} = req.query;
const analytics = await aggregator.getAnalytics({
projectId: projectId as string | undefined,
userId: userId as string | undefined,
startDate: startDate as string | undefined,
endDate: endDate as string | undefined,
groupBy: (groupBy as 'day' | 'week' | 'month') || 'day',
});
res.json(analytics);
} catch (error) {
logger.error('Get analytics error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to fetch analytics',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/analytics/top-expensive
*/
private async handleGetTopExpensive(req: Request, res: Response): Promise<void> {
try {
const { AnalyticsAggregator } = await import('../analytics/aggregator.js');
const aggregator = new AnalyticsAggregator(db.getPool());
const { projectId, limit } = req.query;
if (!projectId) {
res.status(400).json({ error: 'Missing projectId' });
return;
}
const topExpensive = await aggregator.getTopExpensiveRequests(
projectId as string,
limit ? parseInt(limit as string) : 10,
);
res.json(topExpensive);
} catch (error) {
logger.error('Get top expensive error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to fetch top expensive requests',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/analytics/error-rate
*/
private async handleGetErrorRate(req: Request, res: Response): Promise<void> {
try {
const { AnalyticsAggregator } = await import('../analytics/aggregator.js');
const aggregator = new AnalyticsAggregator(db.getPool());
const { projectId } = req.query;
if (!projectId) {
res.status(400).json({ error: 'Missing projectId' });
return;
}
const errorRate = await aggregator.getErrorRateByModel(projectId as string);
res.json(errorRate);
} catch (error) {
logger.error('Get error rate error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to fetch error rate',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/quota/status
*/
private async handleGetQuotaStatus(req: Request, res: Response): Promise<void> {
try {
const { QuotaEnforcer } = await import('../quota/enforcer.js');
const enforcer = new QuotaEnforcer(db.getPool());
const { userId, projectId } = req.query;
if (!userId || !projectId) {
res.status(400).json({ error: 'Missing userId or projectId' });
return;
}
const status = await enforcer.getQuotaStatus(
userId as string,
projectId as string,
);
if (!status) {
res.status(404).json({ error: 'Quota not found' });
return;
}
res.json(status);
} catch (error) {
logger.error('Get quota status error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to fetch quota status',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/quota/update
*/
private async handleUpdateQuota(req: Request, res: Response): Promise<void> {
try {
const { QuotaEnforcer } = await import('../quota/enforcer.js');
const enforcer = new QuotaEnforcer(db.getPool());
const { userId, projectId, maxTokensDaily, maxCostDaily } = req.body;
if (!userId || !projectId || !maxTokensDaily || !maxCostDaily) {
res.status(400).json({
error: 'Missing required fields: userId, projectId, maxTokensDaily, maxCostDaily',
});
return;
}
await enforcer.updateQuotaLimits(
userId,
projectId,
maxTokensDaily,
maxCostDaily,
);
res.json({ success: true, message: 'Quota updated successfully' });
} catch (error) {
logger.error('Update quota error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to update quota',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/traces/:traceId
*/
private async handleGetTrace(req: Request, res: Response): Promise<void> {
try {
const { getTracer } = await import('../tracing/tracer.js');
const tracer = getTracer();
const { traceId } = req.params;
const trace = await tracer.getTrace(traceId);
if (!trace) {
res.status(404).json({ error: 'Trace not found' });
return;
}
res.json(trace);
} catch (error) {
logger.error('Get trace error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to fetch trace',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle semantic code search
*/
private async handleSemanticSearch(req: Request, res: Response): Promise<void> {
try {
const { query, limit = 10, filters } = req.body;
if (!query) {
res.status(400).json({ error: 'Query is required' });
return;
}
const search = new SemanticSearch(db.getPool());
const results = await search.search(query, limit, filters);
res.json({
query,
results,
count: results.length,
});
} catch (error) {
logger.error('Semantic search failed', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Semantic search failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle code indexing
*/
private async handleIndexCode(req: Request, res: Response): Promise<void> {
try {
const { filePath, code, language } = req.body;
if (!filePath || !code || !language) {
res.status(400).json({
error: 'filePath, code, and language are required',
});
return;
}
const search = new SemanticSearch(db.getPool());
await search.indexCodeFile(filePath, code, language);
res.json({
success: true,
message: `Indexed ${filePath}`,
});
} catch (error) {
logger.error('Code indexing failed', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Code indexing failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle search statistics
*/
private async handleSearchStats(_req: Request, res: Response): Promise<void> {
try {
const search = new SemanticSearch(db.getPool());
const stats = await search.getStatistics();
res.json(stats);
} catch (error) {
logger.error('Failed to get search stats', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get search stats',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle knowledge pack creation
*/
private async handleCreateKnowledgePack(req: Request, res: Response): Promise<void> {
try {
const { name, description, files, tags = [] } = req.body;
if (!name || !files || !Array.isArray(files)) {
res.status(400).json({
error: 'name and files array are required',
});
return;
}
const search = new SemanticSearch(db.getPool());
const packManager = new KnowledgePackManager(db.getPool(), search);
const pack = await packManager.createPack(name, description, files, tags);
res.json({
success: true,
pack,
});
} catch (error) {
logger.error('Failed to create knowledge pack', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to create knowledge pack',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle knowledge pack loading
*/
private async handleLoadKnowledgePack(req: Request, res: Response): Promise<void> {
try {
const { packId } = req.params;
const search = new SemanticSearch(db.getPool());
const packManager = new KnowledgePackManager(db.getPool(), search);
const result = await packManager.loadPack(packId);
res.json(result);
} catch (error) {
logger.error('Failed to load knowledge pack', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to load knowledge pack',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle knowledge pack search by tags
*/
private async handleSearchKnowledgePacks(req: Request, res: Response): Promise<void> {
try {
const tags = req.query.tags as string | string[] | undefined;
if (!tags) {
res.status(400).json({ error: 'tags parameter is required' });
return;
}
const tagArray = Array.isArray(tags) ? tags : [tags];
const search = new SemanticSearch(db.getPool());
const packManager = new KnowledgePackManager(db.getPool(), search);
const packs = await packManager.searchByTags(tagArray);
res.json({
tags: tagArray,
packs,
count: packs.length,
});
} catch (error) {
logger.error('Failed to search knowledge packs', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to search knowledge packs',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/models - Get all models with details
*/
private async handleGetModels(_req: Request, res: Response): Promise<void> {
try {
const { MODEL_CATALOG } = await import('../config/models.js');
res.json({
models: MODEL_CATALOG.map(m => ({
id: m.id,
provider: m.provider,
apiModelName: m.apiModelName,
layer: m.layer,
relativeCost: m.relativeCost,
capabilities: m.capabilities,
contextWindow: m.contextWindow,
enabled: m.enabled,
})),
});
} catch (error) {
logger.error('Failed to get models', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get models',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/models/layers - Get all layers with models
*/
private async handleGetLayers(_req: Request, res: Response): Promise<void> {
try {
const { LAYERS_IN_ORDER } = await import('../config/models.js');
const layers: Record<string, {
enabled: boolean;
models: Array<{
id: string;
provider: string;
apiModelName: string;
enabled: boolean;
}>;
providers: string[];
}> = {};
for (const layer of LAYERS_IN_ORDER) {
const models = await modelConfigService.getAllModelsByLayer(layer); // Use getAllModelsByLayer for admin
const providers = Array.from(new Set(models.map(m => m.provider)));
layers[layer] = {
enabled: await modelConfigService.isLayerEnabled(layer),
models: models.map(m => ({
id: m.id,
provider: m.provider,
apiModelName: m.apiModelName,
enabled: m.enabled,
priority: m.priority ?? 0,
})),
providers,
};
}
res.json({ layers });
} catch (error) {
logger.error('Failed to get layers', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get layers',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle PUT /v1/models/:modelId - Update model configuration
*/
private async handleUpdateModel(req: Request, res: Response): Promise<void> {
try {
const { modelId } = req.params;
const updates = req.body;
const model = await modelConfigService.getModelById(modelId);
if (!model) {
res.status(404).json({ error: 'Model not found' });
return;
}
// If only enabled field is provided, use setModelEnabled for backward compatibility
if (Object.keys(updates).length === 1 && updates.enabled !== undefined) {
await modelConfigService.setModelEnabled(modelId, updates.enabled);
} else {
// Full model update
await modelConfigService.updateModel(modelId, updates);
}
const updatedModel = await modelConfigService.getModelById(modelId);
res.json({
success: true,
model: {
id: updatedModel!.id,
provider: updatedModel!.provider,
apiModelName: updatedModel!.apiModelName,
layer: updatedModel!.layer,
relativeCost: updatedModel!.relativeCost,
pricePer1kInputTokens: updatedModel!.pricePer1kInputTokens,
pricePer1kOutputTokens: updatedModel!.pricePer1kOutputTokens,
contextWindow: updatedModel!.contextWindow,
enabled: updatedModel!.enabled,
priority: updatedModel!.priority,
capabilities: updatedModel!.capabilities,
},
});
} catch (error) {
logger.error('Failed to update model', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to update model',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/models - Add new model
*/
private async handleAddModel(req: Request, res: Response): Promise<void> {
try {
const { id, provider, apiModelName, layer, enabled } = req.body;
if (!id || !provider || !apiModelName || !layer) {
res.status(400).json({
error: 'Missing required fields: id, provider, apiModelName, layer',
});
return;
}
// Check if model already exists
const existingModel = await modelConfigService.getModelById(id);
if (existingModel) {
res.status(409).json({ error: 'Model already exists' });
return;
}
// Add new model
await modelConfigService.addModel({
id,
provider,
apiModelName,
layer,
relativeCost: 0,
capabilities: { code: true, general: true, reasoning: false },
contextWindow: 8192,
enabled: enabled !== undefined ? enabled : true,
});
res.json({
success: true,
model: { id, provider, apiModelName, layer, enabled },
});
} catch (error) {
logger.error('Failed to add model', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to add model',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle DELETE /v1/models/:modelId - Delete model
*/
private async handleDeleteModel(req: Request, res: Response): Promise<void> {
try {
const { modelId } = req.params;
const model = await modelConfigService.getModelById(modelId);
if (!model) {
res.status(404).json({ error: 'Model not found' });
return;
}
await modelConfigService.deleteModel(modelId);
res.json({ success: true, modelId });
} catch (error) {
logger.error('Failed to delete model', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to delete model',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle PUT /v1/layers/:layerId/reorder - Reorder models in a layer
*/
private async handleReorderModels(req: Request, res: Response): Promise<void> {
try {
const { layerId } = req.params;
const { modelIds } = req.body;
// Validate layer ID
if (!['L0', 'L1', 'L2', 'L3'].includes(layerId)) {
res.status(400).json({
error: 'Invalid layer ID. Must be L0, L1, L2, or L3',
});
return;
}
if (!Array.isArray(modelIds) || modelIds.length === 0) {
res.status(400).json({
error: 'modelIds must be a non-empty array of model IDs',
});
return;
}
// Reorder models via modelConfigService
await modelConfigService.reorderModels(layerId as 'L0' | 'L1' | 'L2' | 'L3', modelIds);
logger.info(`Reordered ${modelIds.length} models in layer ${layerId}`);
res.json({
success: true,
layer: layerId,
modelIds,
message: `Models in ${layerId} reordered successfully`,
});
} catch (error) {
logger.error('Failed to reorder models', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to reorder models',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle PUT /v1/layers/:layerId/toggle - Toggle layer enable/disable
*/
private async handleToggleLayer(req: Request, res: Response): Promise<void> {
try {
const { layerId } = req.params;
const { enabled } = req.body;
if (enabled === undefined) {
res.status(400).json({
error: 'Missing required field: enabled',
});
return;
}
// Validate layer ID
if (!['L0', 'L1', 'L2', 'L3'].includes(layerId)) {
res.status(400).json({
error: 'Invalid layer ID. Must be L0, L1, L2, or L3',
});
return;
}
// Update layer via modelConfigService
await modelConfigService.setLayerEnabled(layerId as 'L0' | 'L1' | 'L2' | 'L3', enabled);
logger.info(`Layer ${layerId} ${enabled ? 'enabled' : 'disabled'}`);
res.json({
success: true,
layer: layerId,
enabled,
message: `Layer ${layerId} ${enabled ? 'enabled' : 'disabled'} successfully`,
});
} catch (error) {
logger.error('Failed to toggle layer', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to toggle layer',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/providers - Get all providers
*/
private async handleGetProviders(_req: Request, res: Response): Promise<void> {
try {
const providers = [
{
id: 'openai',
name: 'OpenAI',
description: 'GPT models (GPT-4, GPT-4-turbo, GPT-3.5)',
enabled: !!env.OPENAI_API_KEY,
apiKey: env.OPENAI_API_KEY ? `${env.OPENAI_API_KEY.substring(0, 10)}...` : '',
baseUrl: 'https://api.openai.com/v1',
isDefault: true,
},
{
id: 'anthropic',
name: 'Anthropic',
description: 'Claude models (Claude 3.5 Sonnet, Haiku, Opus)',
enabled: !!env.ANTHROPIC_API_KEY,
apiKey: env.ANTHROPIC_API_KEY ? `${env.ANTHROPIC_API_KEY.substring(0, 10)}...` : '',
baseUrl: 'https://api.anthropic.com/v1',
isDefault: true,
},
{
id: 'openrouter',
name: 'OpenRouter',
description: 'Multi-provider API gateway',
enabled: !!env.OPENROUTER_API_KEY,
apiKey: env.OPENROUTER_API_KEY ? `${env.OPENROUTER_API_KEY.substring(0, 10)}...` : '',
baseUrl: 'https://openrouter.ai/api/v1',
isDefault: true,
},
{
id: 'oss-local',
name: 'OSS Local',
description: 'Self-hosted open-source models',
enabled: env.OSS_MODEL_ENABLED,
apiKey: '',
baseUrl: env.OSS_MODEL_ENDPOINT,
isDefault: true,
},
];
res.json({ providers });
} catch (error) {
logger.error('Failed to get providers', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get providers',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/providers - Add custom provider
*/
private async handleAddProvider(req: Request, res: Response): Promise<void> {
try {
const { id, name, description, apiKey, baseUrl, apiFunction } = req.body;
if (!id || !name || !baseUrl) {
res.status(400).json({
error: 'Missing required fields: id, name, baseUrl',
});
return;
}
// In real implementation, save to database
// For now, return success
res.json({
success: true,
provider: {
id,
name,
description,
apiKey: apiKey ? `${apiKey.substring(0, 10)}...` : '',
baseUrl,
apiFunction,
isDefault: false,
},
});
} catch (error) {
logger.error('Failed to add provider', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to add provider',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle PUT /v1/providers/:providerId - Update provider
*/
private async handleUpdateProvider(req: Request, res: Response): Promise<void> {
try {
const { providerId } = req.params;
const { apiKey, baseUrl, enabled } = req.body;
// In real implementation, update environment variables or database
res.json({
success: true,
provider: { id: providerId, apiKey: apiKey ? `${apiKey.substring(0, 10)}...` : undefined, baseUrl, enabled },
message: 'Provider update requires restart to take effect',
});
} catch (error) {
logger.error('Failed to update provider', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to update provider',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle DELETE /v1/providers/:providerId - Delete custom provider
*/
private async handleDeleteProvider(req: Request, res: Response): Promise<void> {
try {
const { providerId } = req.params;
// Only allow deleting custom providers (not default ones)
const defaultProviders = ['openai', 'anthropic', 'openrouter', 'oss-local'];
if (defaultProviders.includes(providerId)) {
res.status(400).json({
error: 'Cannot delete default providers',
});
return;
}
// In real implementation, delete from database
res.json({ success: true, providerId });
} catch (error) {
logger.error('Failed to delete provider', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to delete provider',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/gpt-plus/status - Get GPT Plus session status
*/
private async handleGPTPlusStatus(_req: Request, res: Response): Promise<void> {
try {
const isAvailable = gptPlusClient.isAvailable();
const sessionInfo = gptPlusClient.getSessionInfo();
res.json({
available: isAvailable,
session: sessionInfo ? {
email: sessionInfo.email,
isPremium: sessionInfo.isPremium,
expiresAt: sessionInfo.expiresAt,
expiresIn: Math.max(0, Math.floor((sessionInfo.expiresAt.getTime() - Date.now()) / 1000 / 60)), // minutes
} : null,
});
} catch (error) {
logger.error('Failed to get GPT Plus status', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get GPT Plus status',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/gpt-plus/login - Login with GPT Plus access token
*/
private async handleGPTPlusLogin(req: Request, res: Response): Promise<void> {
try {
const { accessToken, email } = req.body;
if (!accessToken || !email) {
res.status(400).json({
error: 'Missing required fields: accessToken, email',
});
return;
}
const result = await gptPlusClient.loginWithAccessToken(accessToken, email);
if (result.success) {
const sessionInfo = gptPlusClient.getSessionInfo();
res.json({
success: true,
session: sessionInfo,
message: 'GPT Plus login successful',
});
} else {
res.status(401).json({
success: false,
error: result.error || 'Login failed',
});
}
} catch (error) {
logger.error('GPT Plus login failed', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'GPT Plus login failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/gpt-plus/logout - Logout from GPT Plus
*/
private async handleGPTPlusLogout(_req: Request, res: Response): Promise<void> {
try {
await gptPlusClient.logout();
res.json({
success: true,
message: 'GPT Plus logged out successfully',
});
} catch (error) {
logger.error('GPT Plus logout failed', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'GPT Plus logout failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/gpt-plus/models - Get available GPT Plus models
*/
private async handleGPTPlusModels(_req: Request, res: Response): Promise<void> {
try {
const models = gptPlusClient.getAvailableModels();
res.json({
available: gptPlusClient.isAvailable(),
models,
});
} catch (error) {
logger.error('Failed to get GPT Plus models', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get GPT Plus models',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/gpt-plus/chat - Chat with GPT Plus
*/
private async handleGPTPlusChat(req: Request, res: Response): Promise<void> {
try {
if (!gptPlusClient.isAvailable()) {
res.status(401).json({
error: 'GPT Plus session not available. Please login first.',
});
return;
}
const { messages, model, conversationId, parentMessageId } = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
res.status(400).json({
error: 'Missing required field: messages (array)',
});
return;
}
const result = await gptPlusClient.chat(messages, {
model,
conversationId,
parentMessageId,
});
res.json({
success: true,
response: result.content,
conversationId: result.conversationId,
messageId: result.messageId,
});
} catch (error) {
logger.error('GPT Plus chat failed', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'GPT Plus chat failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/terminal/sessions - Get all terminal sessions
*/
private async handleGetTerminalSessions(_req: Request, res: Response): Promise<void> {
try {
const sessions = terminalManager.getAllSessions();
res.json({ sessions });
} catch (error) {
logger.error('Failed to get terminal sessions', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get terminal sessions',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/local - Create local shell session
*/
private async handleCreateLocalSession(_req: Request, res: Response): Promise<void> {
try {
const session = await terminalManager.createLocalSession();
res.json({ success: true, session });
} catch (error) {
logger.error('Failed to create local session', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to create local session',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/ssh - Create SSH session
*/
private async handleCreateSSHSession(req: Request, res: Response): Promise<void> {
try {
const { host, port, username, password, privateKey, passphrase } = req.body;
if (!host || !username) {
res.status(400).json({
error: 'Missing required fields: host, username',
});
return;
}
if (!password && !privateKey) {
res.status(400).json({
error: 'Either password or privateKey is required',
});
return;
}
const session = await terminalManager.createSSHSession({
host,
port: port || 22,
username,
password,
privateKey,
passphrase,
});
res.json({ success: true, session });
} catch (error) {
logger.error('Failed to create SSH session', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to create SSH session',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/telnet - Create Telnet session
*/
private async handleCreateTelnetSession(req: Request, res: Response): Promise<void> {
try {
const { host, port } = req.body;
if (!host) {
res.status(400).json({
error: 'Missing required field: host',
});
return;
}
const session = await terminalManager.createTelnetSession({
host,
port: port || 23,
});
res.json({ success: true, session });
} catch (error) {
logger.error('Failed to create Telnet session', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to create Telnet session',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/:sessionId/execute - Execute command in session
*/
private async handleExecuteCommand(req: Request, res: Response): Promise<void> {
try {
const { sessionId } = req.params;
const { command } = req.body;
if (!command) {
res.status(400).json({
error: 'Missing required field: command',
});
return;
}
const session = terminalManager.getSession(sessionId);
if (!session) {
res.status(404).json({
error: 'Session not found',
});
return;
}
let result;
if (session.type === 'local') {
result = await terminalManager.executeLocalCommand(sessionId, command);
} else if (session.type === 'ssh') {
result = await terminalManager.executeSSHCommand(sessionId, command);
} else {
res.status(400).json({
error: 'Telnet sessions use send endpoint instead of execute',
});
return;
}
res.json({ success: true, result });
} catch (error) {
logger.error('Failed to execute command', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to execute command',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/:sessionId/send - Send data to Telnet session
*/
private async handleSendData(req: Request, res: Response): Promise<void> {
try {
const { sessionId } = req.params;
const { data } = req.body;
logger.info('Terminal send data request', { sessionId, data: JSON.stringify(data) });
if (!data) {
res.status(400).json({
error: 'Missing required field: data',
});
return;
}
const session = terminalManager.getSession(sessionId);
if (!session) {
logger.warn('Terminal session not found', { sessionId });
res.status(404).json({
error: 'Session not found',
});
return;
}
// Support both Telnet and SSH interactive sessions
if (session.type === 'telnet') {
logger.info('Sending to Telnet session', { sessionId, data: JSON.stringify(data) });
await terminalManager.sendTelnetData(sessionId, data);
} else if (session.type === 'ssh') {
logger.info('Sending to SSH session', { sessionId, data: JSON.stringify(data) });
await terminalManager.sendToSSH(sessionId, data);
} else {
res.status(400).json({
error: 'Send is only for Telnet/SSH sessions. Use execute for local.',
});
return;
}
res.json({ success: true });
} catch (error) {
logger.error('Failed to send data', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to send data',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/terminal/:sessionId/output - Get session output buffer
*/
private async handleGetSessionOutput(req: Request, res: Response): Promise<void> {
try {
const { sessionId } = req.params;
const { clear } = req.query;
const session = terminalManager.getSession(sessionId);
if (!session) {
res.status(404).json({
error: 'Session not found',
});
return;
}
const output = terminalManager.getSessionOutput(sessionId);
// Debug: Log when there's output to return
if (output.length > 0) {
logger.info(`[API] Returning output for ${sessionId}:`, {
chunks: output.length,
totalLength: output.join('').length,
preview: output.join('').substring(0, 200),
clear: clear === 'true'
});
}
if (clear === 'true') {
terminalManager.clearSessionOutput(sessionId);
}
res.json({ success: true, output, session });
} catch (error) {
logger.error('Failed to get session output', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get session output',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle DELETE /v1/terminal/:sessionId - Close session
*/
private async handleCloseSession(req: Request, res: Response): Promise<void> {
try {
const { sessionId } = req.params;
const session = terminalManager.getSession(sessionId);
if (!session) {
res.status(404).json({
error: 'Session not found',
});
return;
}
await terminalManager.closeSession(sessionId);
res.json({ success: true, message: 'Session closed' });
} catch (error) {
logger.error('Failed to close session', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to close session',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
// =====================================
// Terminal Connection Profiles Handlers
// =====================================
private terminalConnectionService: TerminalConnectionService | null = null;
private async getTerminalConnectionService(): Promise<TerminalConnectionService> {
if (!this.terminalConnectionService) {
if (!db.isReady()) {
throw new Error('Database not ready');
}
this.terminalConnectionService = new TerminalConnectionService(db.getPool());
await this.terminalConnectionService.initialize();
}
return this.terminalConnectionService;
}
/**
* Handle GET /v1/terminal/connections - List all saved connections
*/
private async handleGetTerminalConnections(req: Request, res: Response): Promise<void> {
try {
const service = await this.getTerminalConnectionService();
const connections = await service.getAll();
res.json({ success: true, connections });
} catch (error) {
logger.error('Failed to get terminal connections', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get terminal connections',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/connections - Create a new connection profile
*/
private async handleCreateTerminalConnection(req: Request, res: Response): Promise<void> {
try {
const { name, type, host, port, username, authType, password, privateKey, isDefault, notes, metadata } = req.body;
if (!name || !type) {
res.status(400).json({
error: 'Missing required fields: name, type',
});
return;
}
if (type !== 'local' && !host) {
res.status(400).json({
error: 'Host is required for SSH/Telnet connections',
});
return;
}
const service = await this.getTerminalConnectionService();
const connection = await service.create({
name,
type,
host,
port,
username,
authType,
password,
privateKey,
isDefault,
notes,
metadata,
});
res.json({ success: true, connection });
} catch (error) {
logger.error('Failed to create terminal connection', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to create terminal connection',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle GET /v1/terminal/connections/:connectionId - Get connection by ID
*/
private async handleGetTerminalConnectionById(req: Request, res: Response): Promise<void> {
try {
const { connectionId } = req.params;
const service = await this.getTerminalConnectionService();
const connection = await service.getById(connectionId);
if (!connection) {
res.status(404).json({
error: 'Connection not found',
});
return;
}
res.json({ success: true, connection });
} catch (error) {
logger.error('Failed to get terminal connection', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to get terminal connection',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle PUT /v1/terminal/connections/:connectionId - Update connection
*/
private async handleUpdateTerminalConnection(req: Request, res: Response): Promise<void> {
try {
const { connectionId } = req.params;
const { name, host, port, username, authType, password, privateKey, isDefault, notes, metadata } = req.body;
const service = await this.getTerminalConnectionService();
const connection = await service.update(connectionId, {
name,
host,
port,
username,
authType,
password,
privateKey,
isDefault,
notes,
metadata,
});
if (!connection) {
res.status(404).json({
error: 'Connection not found',
});
return;
}
res.json({ success: true, connection });
} catch (error) {
logger.error('Failed to update terminal connection', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to update terminal connection',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle DELETE /v1/terminal/connections/:connectionId - Delete connection
*/
private async handleDeleteTerminalConnection(req: Request, res: Response): Promise<void> {
try {
const { connectionId } = req.params;
const service = await this.getTerminalConnectionService();
const deleted = await service.delete(connectionId);
if (!deleted) {
res.status(404).json({
error: 'Connection not found',
});
return;
}
res.json({ success: true, message: 'Connection deleted' });
} catch (error) {
logger.error('Failed to delete terminal connection', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to delete terminal connection',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle POST /v1/terminal/connections/:connectionId/connect - Create session from profile
*/
private async handleConnectFromProfile(req: Request, res: Response): Promise<void> {
try {
const { connectionId } = req.params;
const service = await this.getTerminalConnectionService();
const connection = await service.getById(connectionId);
if (!connection) {
res.status(404).json({
error: 'Connection not found',
});
return;
}
// Get credentials
const credentials = await service.getCredentials(connectionId);
if (connection.type === 'local') {
const session = await terminalManager.createLocalSession();
res.json({ success: true, session, connectionId });
} else if (connection.type === 'ssh') {
if (!connection.host || !connection.username) {
res.status(400).json({
error: 'SSH connection requires host and username',
});
return;
}
const session = await terminalManager.createSSHSession({
host: connection.host,
port: connection.port || 22,
username: connection.username,
password: credentials?.password,
privateKey: credentials?.privateKey,
});
res.json({ success: true, session, connectionId });
} else if (connection.type === 'telnet') {
if (!connection.host) {
res.status(400).json({
error: 'Telnet connection requires host',
});
return;
}
const session = await terminalManager.createTelnetSession({
host: connection.host,
port: connection.port || 23,
});
res.json({ success: true, session, connectionId });
} else {
res.status(400).json({
error: 'Invalid connection type',
});
}
} catch (error) {
logger.error('Failed to connect from profile', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Failed to connect from profile',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
// ==================
// Auth Handler Methods
// ==================
/**
* Auth middleware - validates JWT token when auth is enabled
*/
private async authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
// Skip auth if disabled
if (!authService.isAuthEnabled()) {
next();
return;
}
// Skip auth for certain public endpoints
const publicPaths = ['/v1/health', '/v1/auth/'];
if (publicPaths.some(path => req.path.startsWith(path))) {
next();
return;
}
// Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({
error: 'Authentication required',
message: 'Please provide a valid Bearer token in the Authorization header',
});
return;
}
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
const payload = authService.verifyToken(token);
if (!payload) {
res.status(401).json({
error: 'Invalid or expired token',
message: 'Please login again to get a new token',
});
return;
}
// Attach user info to request for later use
(req as any).user = payload;
next();
}
/**
* Handle login request
*/
private async handleLogin(req: Request, res: Response): Promise<void> {
try {
// Ensure auth service is initialized (lazy init if db wasn't ready at startup)
const authReady = await this.ensureAuthServiceInit();
if (!authReady) {
res.status(503).json({
error: 'Authentication service not available',
details: 'Database connection not ready. Please try again later.',
});
return;
}
const { username, password } = req.body;
if (!username || !password) {
res.status(400).json({
error: 'Username and password are required',
});
return;
}
const result = await authService.login(username, password);
if (result.success) {
res.json({
success: true,
user: result.user,
token: result.token,
expiresIn: env.ADMIN_SESSION_EXPIRY,
});
} else {
res.status(401).json({
success: false,
error: result.error,
});
}
} catch (error) {
logger.error('Login error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Authentication failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Handle verify token request
*/
private async handleVerifyToken(req: Request, res: Response): Promise<void> {
try {
// Ensure auth service is initialized
const authReady = await this.ensureAuthServiceInit();
if (!authReady) {
res.status(503).json({
error: 'Authentication service not available',
details: 'Database connection not ready. Please try again later.',
});
return;
}
const { token } = req.body;
if (!token) {
res.status(400).json({
error: 'Token is required',
});
return;
}
const payload = authService.verifyToken(token);
if (payload) {
// Get user info
const user = await authService.getUserById(payload.userId);
res.json({
valid: true,
user,
payload,
});
} else {
res.status(401).json({
valid: false,
error: 'Invalid or expired token',
});
}
} catch (error) {
logger.error('Verify token error', {
error: error instanceof Error ? error.message : 'Unknown',
});
res.status(500).json({
error: 'Token verification failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Start the API server
*/
async start(): Promise<void> {
const port = parseInt(env.API_PORT);
const host = env.API_HOST;
// Wait for routes to be set up
await this.routesReady;
// Check LLM provider connectivity using provider health manager
await providerHealth.refreshAllProviders();
// Wait for database to be ready (with timeout)
const dbReadyTimeout = 10000; // 10 seconds
const startTime = Date.now();
while (!db.isReady() && Date.now() - startTime < dbReadyTimeout) {
await new Promise(resolve => setTimeout(resolve, 500));
}
if (db.isReady()) {
// Initialize database schema
await db.initSchema();
// Initialize model configurations from database
await modelConfigService.initialize();
// Initialize GPT Plus client (load session from DB)
await gptPlusClient.initialize();
} else {
logger.warn('Database not ready after timeout, skipping DB initialization');
}
this.server = this.app.listen(port, host, () => {
logger.info('API server started', {
host,
port,
endpoints: [
'GET /health',
'GET /admin/*',
'POST /v1/route',
'POST /v1/code-agent',
'POST /v1/chat',
'GET /v1/context/:conversationId',
'POST /v1/context/:conversationId',
'POST /v1/cache/clear',
'GET /v1/stats',
'GET /v1/stats/conversation/:conversationId',
'GET /v1/server-stats',
'POST /v1/mcp-cli',
'POST /v1/search/code',
'POST /v1/search/index',
'GET /v1/search/stats',
'POST /v1/knowledge/pack',
'GET /v1/knowledge/pack/:packId',
'GET /v1/knowledge/search',
'GET /v1/models',
'GET /v1/models/layers',
'PUT /v1/models/:modelId',
'POST /v1/models',
'DELETE /v1/models/:modelId',
'PUT /v1/layers/:layerId/toggle',
'GET /v1/providers',
'POST /v1/providers',
'PUT /v1/providers/:providerId',
'DELETE /v1/providers/:providerId',
'GET /v1/openrouter/models',
'GET /v1/openrouter/limits',
'GET /v1/openrouter/credits',
'GET /v1/openrouter/activity',
],
});
});
}
/**
* Stop the API server
*/
async stop(): Promise<void> {
if (this.server) {
this.server.close();
logger.info('API server stopped');
}
}
}
// Singleton instance
export const apiServer = new APIServer();