UNPKG

tryaii-mcp-server

Version:

TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence

964 lines 47.2 kB
import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import { logger, loggers } from '../utils/logger.js'; import { config } from '../utils/config.js'; import { SessionService } from './SessionService.js'; import { encrypt, extractUserFromApiKey, maskApiKey } from '../utils/encryption.js'; export class SessionManager extends EventEmitter { sessions = new Map(); SESSION_TIMEOUT = config.session.timeoutMinutes * 60 * 1000; // Convert to milliseconds CLEANUP_INTERVAL = config.session.cleanupIntervalMinutes * 60 * 1000; MAX_SESSIONS = config.session.maxConcurrentSessions; cleanupTimer = null; running = false; metrics = { totalSessionsCreated: 0, totalSessionsExpired: 0, totalRequestsProcessed: 0, averageSessionDuration: 0, peakConcurrentSessions: 0 }; // Dependency injection for HTTP client and session service httpClient = null; sessionService = SessionService.getInstance(); constructor() { super(); this.setupEventHandlers(); } setupEventHandlers() { this.on('sessionCreated', (sessionId, userId) => { logger.info('Session created', { sessionId, userId }); }); this.on('sessionExpired', (sessionId, reason) => { logger.info('Session expired', { sessionId, reason }); }); this.on('requestQueued', (sessionId, requestId) => { logger.debug('Request queued', { sessionId, requestId }); }); this.on('requestProcessed', (sessionId, requestId, success) => { logger.debug('Request processed', { sessionId, requestId, success }); }); } start() { if (this.running) { logger.warn('Session manager is already running'); return; } this.running = true; this.startCleanupScheduler(); logger.info('Session manager started', { sessionTimeout: this.SESSION_TIMEOUT, cleanupInterval: this.CLEANUP_INTERVAL, maxSessions: this.MAX_SESSIONS }); } stop() { if (!this.running) { return; } this.running = false; if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } // Cancel all pending requests and expire all sessions for (const [sessionId, session] of this.sessions) { this.expireSession(sessionId, 'shutdown'); } logger.info('Session manager stopped'); } async createSession(userId, apiKeyId, tryaiiApiKey) { // Check if we've reached the max session limit if (this.sessions.size >= this.MAX_SESSIONS) { // Try to clean up expired sessions first this.cleanupExpiredSessions(); if (this.sessions.size >= this.MAX_SESSIONS) { throw new Error(`Maximum concurrent sessions limit reached (${this.MAX_SESSIONS})`); } } const sessionId = uuidv4(); const now = new Date(); // Extract and encrypt user information from TryAII API key let encryptedUserData = null; let userInfo = { userId: 'anonymous', provider: 'unknown' }; if (tryaiiApiKey) { try { userInfo = extractUserFromApiKey(tryaiiApiKey); encryptedUserData = encrypt(tryaiiApiKey); logger.info('User identified from API key', { userId: userInfo.userId, provider: userInfo.provider, maskedKey: maskApiKey(tryaiiApiKey) }); } catch (error) { logger.warn('Failed to process TryAII API key', { error }); } } const session = { sessionId, userId: userInfo.userId, // Use extracted user ID apiKeyId, createdAt: now, lastActivity: now, status: 'active', requestQueue: { pending: [], processing: null, completed: [] }, // Add encrypted user data for tracking encryptedUserData, userProvider: userInfo.provider }; this.sessions.set(sessionId, session); this.metrics.totalSessionsCreated++; // Update peak concurrent sessions if (this.sessions.size > this.metrics.peakConcurrentSessions) { this.metrics.peakConcurrentSessions = this.sessions.size; } this.emit('sessionCreated', sessionId, userInfo.userId); loggers.sessionCreated(sessionId, userInfo.userId, apiKeyId); return sessionId; } async getOrCreateSession(userId, apiKeyId, tryaiiApiKey) { // 🔥 ENHANCED: Use real user data instead of extracting from TryAII API key // The userId and apiKeyId are already validated by CrossDatabaseService // Find existing active session for this user/apiKeyId combination for (const [sessionId, session] of this.sessions) { if (session.userId === userId && session.apiKeyId === apiKeyId && session.status === 'active' && !this.isSessionExpired(session)) { // Update last activity session.lastActivity = new Date(); logger.debug('Reusing existing session', { sessionId, userId, apiKeyId }); return session; } } // Create new session with real user data const sessionId = await this.createSessionWithUserContext(userId, apiKeyId, tryaiiApiKey); const session = this.sessions.get(sessionId); return session; } /** * 🔥 ENHANCED: Create session with user context data */ async createSessionWithUserContext(userId, apiKeyId, tryaiiApiKey) { // Check if we've reached the max session limit if (this.sessions.size >= this.MAX_SESSIONS) { // Try to clean up expired sessions first this.cleanupExpiredSessions(); if (this.sessions.size >= this.MAX_SESSIONS) { throw new Error(`Maximum concurrent sessions limit reached (${this.MAX_SESSIONS})`); } } const sessionId = uuidv4(); const now = new Date(); // 🔥 ENHANCED: Get user context from CrossDatabaseService const { crossDatabaseService } = await import('./CrossDatabaseService.js'); // Get user details by finding the API key let userContext = { keyPrefix: '', userName: undefined, userEmail: undefined, permissions: [], connectionSource: 'mcp', clientVersion: undefined }; try { // Extract keyPrefix from apiKeyId for lookup (simplified approach) // In a real implementation, we might need to query the database differently logger.debug('Creating session with enhanced user context', { userId, apiKeyId }); // For now, we'll populate what we can - the authentication already happened // so we know this is a valid user/key combination userContext = { keyPrefix: apiKeyId.substring(0, 8), // Simplified - would need proper lookup userName: undefined, // Would be populated by authentication flow userEmail: undefined, // Would be populated by authentication flow permissions: [], // Would be populated by authentication flow connectionSource: 'mcp', clientVersion: undefined }; } catch (error) { logger.warn('Failed to get user context, using defaults', { error, userId, apiKeyId }); } // Handle legacy TryAII API key processing for backward compatibility let encryptedUserData = null; let userInfo = { userId, provider: 'mcp-auth' }; if (tryaiiApiKey) { try { userInfo = extractUserFromApiKey(tryaiiApiKey); encryptedUserData = encrypt(tryaiiApiKey); logger.info('Legacy TryAII API key processed', { userId: userInfo.userId, provider: userInfo.provider, maskedKey: maskApiKey(tryaiiApiKey) }); } catch (error) { logger.warn('Failed to process TryAII API key', { error }); } } const session = { sessionId, userId: userId, // Use the authenticated user ID apiKeyId, createdAt: now, lastActivity: now, status: 'active', requestQueue: { pending: [], processing: null, completed: [] }, // Legacy support encryptedUserData, userProvider: userInfo.provider, // 🔥 ENHANCED: Add user context userContext }; this.sessions.set(sessionId, session); this.metrics.totalSessionsCreated++; // Update peak concurrent sessions if (this.sessions.size > this.metrics.peakConcurrentSessions) { this.metrics.peakConcurrentSessions = this.sessions.size; } this.emit('sessionCreated', sessionId, userId); loggers.sessionCreated(sessionId, userId, apiKeyId); logger.info('Enhanced session created', { sessionId, userId, apiKeyId, keyPrefix: userContext.keyPrefix, permissions: userContext.permissions, connectionSource: userContext.connectionSource }); return sessionId; } async queueRequest(sessionId, method, params) { const session = this.sessions.get(sessionId); if (!session) { throw new Error('Session not found'); } if (this.isSessionExpired(session)) { this.expireSession(sessionId, 'timeout'); throw new Error('Session expired'); } // Update session activity session.lastActivity = new Date(); const requestId = uuidv4(); return new Promise((resolve, reject) => { const pendingRequest = { id: requestId, method, params, timestamp: new Date(), resolve, reject }; // Add timeout for the request pendingRequest.timeout = setTimeout(() => { this.rejectRequest(sessionId, requestId, new Error('Request timeout')); }, config.mcp.timeout); session.requestQueue.pending.push(pendingRequest); this.emit('requestQueued', sessionId, requestId); // Process queue if nothing is currently processing if (!session.requestQueue.processing) { this.processNextRequest(sessionId); } }); } async processNextRequest(sessionId) { const session = this.sessions.get(sessionId); if (!session || session.requestQueue.pending.length === 0) { return; } const pendingRequest = session.requestQueue.pending.shift(); if (!pendingRequest) { return; } const processingRequest = { id: pendingRequest.id, method: pendingRequest.method, params: pendingRequest.params, startTime: new Date() }; session.requestQueue.processing = processingRequest; try { let result; // Call the HTTP client based on the method if (this.httpClient && this.httpClient.isClientHealthy()) { switch (pendingRequest.method) { case 'list_available_models': result = await this.httpClient.listAvailableModels(pendingRequest.params?.provider || undefined); break; case 'get_model_info': result = await this.httpClient.getModelInfo(pendingRequest.params?.modelId); break; case 'chat_with_model': result = await this.httpClient.chatWithModel(pendingRequest.params); break; case 'compare_models': const compareResult = await this.httpClient.compareModels(pendingRequest.params); // The HTTP endpoint returns results directly, wrap for backward compatibility result = { results: compareResult }; break; case 'brains': result = await this.httpClient.brains(pendingRequest.params); break; default: throw new Error(`Unknown method: ${pendingRequest.method}`); } } else { throw new Error('HTTP client is not available or unhealthy'); } // 🚀 FIX: Calculate and attach totalCost to result for McpProtocolHandler let calculatedTotalCost = 0; if (result && typeof result === 'object') { const modelInteractions = this.extractModelInteractions(result, pendingRequest.method); calculatedTotalCost = modelInteractions.reduce((sum, i) => sum + (i.cost || 0), 0); // Attach the calculated cost to the result object for easy extraction result._mcpCalculatedCost = calculatedTotalCost; logger.info('🚀 Attached calculated cost to result for McpProtocolHandler', { method: pendingRequest.method, sessionId, requestId: pendingRequest.id, calculatedTotalCost, interactionCount: modelInteractions.length }); } // Clear timeout if (pendingRequest.timeout) { clearTimeout(pendingRequest.timeout); } // Complete the request const completedRequest = { id: pendingRequest.id, method: pendingRequest.method, success: true, result, processingTime: Date.now() - processingRequest.startTime.getTime(), completedAt: new Date() }; session.requestQueue.completed.push(completedRequest); session.requestQueue.processing = null; // Keep only last 10 completed requests to prevent memory bloat if (session.requestQueue.completed.length > 10) { session.requestQueue.completed = session.requestQueue.completed.slice(-10); } this.metrics.totalRequestsProcessed++; this.emit('requestProcessed', sessionId, pendingRequest.id, true); // 🆕 SAVE SESSION DATA TO DATABASE try { await this.saveSessionToDatabase(sessionId, pendingRequest.method, pendingRequest.params, result, completedRequest.processingTime, undefined, // balanceResult will be set by McpProtocolHandler calculatedTotalCost // Pass the calculated cost ); logger.debug('Session data saved to database', { sessionId, method: pendingRequest.method }); } catch (dbError) { logger.error('Failed to save session to database', { sessionId, method: pendingRequest.method, error: dbError instanceof Error ? dbError.message : String(dbError) }); // Don't fail the request if database save fails } // Log successful processing logger.debug('Request processed successfully', { sessionId, requestId: pendingRequest.id, method: pendingRequest.method, processingTime: completedRequest.processingTime, calculatedTotalCost }); // Resolve the original promise pendingRequest.resolve(result); // Process next request if any if (session.requestQueue.pending.length > 0) { setImmediate(() => this.processNextRequest(sessionId)); } } catch (error) { logger.error('Request processing failed', { sessionId, requestId: pendingRequest.id, method: pendingRequest.method, error: error instanceof Error ? error.message : String(error) }); this.rejectRequest(sessionId, pendingRequest.id, error instanceof Error ? error : new Error(String(error))); } } rejectRequest(sessionId, requestId, error) { const session = this.sessions.get(sessionId); if (!session) { return; } // Find and remove from pending queue const pendingIndex = session.requestQueue.pending.findIndex(req => req.id === requestId); if (pendingIndex >= 0) { const pendingRequest = session.requestQueue.pending.splice(pendingIndex, 1)[0]; if (pendingRequest && pendingRequest.timeout) { clearTimeout(pendingRequest.timeout); } if (pendingRequest) { pendingRequest.reject(error); } } // If this was the processing request, clear it if (session.requestQueue.processing?.id === requestId) { session.requestQueue.processing = null; // Process next request if any if (session.requestQueue.pending.length > 0) { setImmediate(() => this.processNextRequest(sessionId)); } } this.emit('requestProcessed', sessionId, requestId, false); } startCleanupScheduler() { this.cleanupTimer = setInterval(() => { this.cleanupExpiredSessions(); }, this.CLEANUP_INTERVAL); } cleanupExpiredSessions() { const expiredSessions = []; for (const [sessionId, session] of this.sessions) { if (this.isSessionExpired(session)) { expiredSessions.push(sessionId); } } for (const sessionId of expiredSessions) { this.expireSession(sessionId, 'timeout'); } if (expiredSessions.length > 0) { logger.info('Cleaned up expired sessions', { expiredCount: expiredSessions.length, remainingSessions: this.sessions.size }); } } isSessionExpired(session) { const now = Date.now(); const lastActivity = session.lastActivity.getTime(); return (now - lastActivity) > this.SESSION_TIMEOUT; } expireSession(sessionId, reason) { const session = this.sessions.get(sessionId); if (!session) { return; } // Cancel all pending requests for (const pendingRequest of session.requestQueue.pending) { if (pendingRequest.timeout) { clearTimeout(pendingRequest.timeout); } pendingRequest.reject(new Error('Session expired')); } // Cancel processing request if (session.requestQueue.processing) { // The processing request will be handled by the MCP client timeout } session.status = 'expired'; this.sessions.delete(sessionId); this.metrics.totalSessionsExpired++; this.emit('sessionExpired', sessionId, reason); loggers.sessionExpired(sessionId, session.userId, reason); } // Public API methods getSession(sessionId) { return this.sessions.get(sessionId); } getActiveSessionCount() { return this.sessions.size; } getSessionStatus(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return null; } const completedRequests = session.requestQueue.completed; const averageWaitTime = completedRequests.length > 0 ? completedRequests.reduce((sum, req) => sum + req.processingTime, 0) / completedRequests.length : 0; return { pending: session.requestQueue.pending.length, processing: session.requestQueue.processing !== null, completed: session.requestQueue.completed.length, averageWaitTime }; } getMetrics() { const activeSessions = Array.from(this.sessions.values()); const totalSessionDuration = activeSessions.reduce((sum, session) => { return sum + (Date.now() - session.createdAt.getTime()); }, 0); this.metrics.averageSessionDuration = activeSessions.length > 0 ? totalSessionDuration / activeSessions.length : 0; return { ...this.metrics, currentActiveSessions: this.sessions.size, totalPendingRequests: activeSessions.reduce((sum, session) => sum + session.requestQueue.pending.length, 0), totalProcessingRequests: activeSessions.filter(session => session.requestQueue.processing !== null).length }; } // Helper methods for health checks isRunning() { return this.running; } setHttpClient(httpClient) { this.httpClient = httpClient; } // 🆕 Save session data to database using SessionService async saveSessionToDatabase(sessionId, method, params, result, processingTime, balanceResult, totalCost) { const session = this.sessions.get(sessionId); if (!session) { throw new Error('Session not found for database save'); } // Get balance information from session if available const sessionBalanceInfo = session.balanceInfo; const finalBalanceResult = balanceResult || sessionBalanceInfo?.balanceResult; // 🔥 CALCULATE ACTUAL COST FROM MODEL INTERACTIONS (authoritative source) let actualTotalCost = 0; if (result && typeof result === 'object') { // 🚀 PRIORITY: Use the cost already calculated and attached by queueRequest if (result._mcpCalculatedCost && typeof result._mcpCalculatedCost === 'number') { actualTotalCost = result._mcpCalculatedCost; logger.debug('Using pre-calculated cost from queueRequest', { sessionId, method, actualTotalCost, providedTotalCost: totalCost }); } else { // Fallback: recalculate if not already done const modelInteractions = this.extractModelInteractions(result, method); actualTotalCost = modelInteractions.reduce((sum, i) => sum + (i.cost || 0), 0); logger.debug('Fallback: Calculated actual total cost from model interactions', { sessionId, method, interactionCount: modelInteractions.length, actualTotalCost, providedTotalCost: totalCost, sessionBalanceTotalCost: sessionBalanceInfo?.totalCost }); } } // Use actual calculated cost as the final authoritative cost const finalTotalCost = actualTotalCost; logger.debug('Save session to database - balance info check', { sessionId, method, hasSessionBalanceInfo: !!sessionBalanceInfo, sessionBalanceInfoContent: sessionBalanceInfo, finalBalanceResult: finalBalanceResult ? 'present' : 'missing', finalTotalCost, actualTotalCost, providedBalanceResult: balanceResult ? 'present' : 'missing', providedTotalCost: totalCost, usingPreCalculatedCost: !!result?._mcpCalculatedCost }); // Prepare enhanced session data with encrypted user tracking const baseSessionData = { userId: session.userId, apiKeyId: session.apiKeyId, clientMetadata: { requestId: sessionId, userProvider: session.userProvider, hasEncryptedUserData: !!session.encryptedUserData, // Store encrypted user data if available encryptedUserData: session.encryptedUserData } }; let dbSessionId; // Create database session based on method type switch (method) { case 'brains': // Create brains session with cost information const sessionCreateData = { question: params.question, enableWebSearch: params.enableWebSearch, temperature: params.temperature, maxTokens: params.maxTokens }; dbSessionId = await this.sessionService.createSession('brains', baseSessionData, sessionCreateData); // Update with actual calculated totalCost after creation if (finalTotalCost > 0) { await this.sessionService.updateSessionCost(dbSessionId, 'brains', finalTotalCost); } // Set cost and balance update using existing public methods if (finalBalanceResult) { await this.sessionService.updateSessionBalance(dbSessionId, 'brains', finalBalanceResult); } // Add response data if available if (result && typeof result === 'object') { const modelInteractions = this.extractModelInteractions(result, 'brains'); for (const interaction of modelInteractions) { await this.sessionService.addModelInteraction(dbSessionId, 'brains', interaction); } } // Complete the session await this.sessionService.completeSession(dbSessionId, 'brains', processingTime, 'success'); break; case 'compare_models': dbSessionId = await this.sessionService.createSession('compare', baseSessionData, { modelIds: params.modelIds, message: params.message, enableWebSearch: params.enableWebSearch, temperature: params.temperature, maxTokens: params.maxTokens }); if (result && typeof result === 'object') { const modelInteractions = this.extractModelInteractions(result, 'compare'); for (const interaction of modelInteractions) { await this.sessionService.addModelInteraction(dbSessionId, 'compare', interaction); } } await this.sessionService.completeSession(dbSessionId, 'compare', processingTime, 'success'); break; case 'chat_with_model': dbSessionId = await this.sessionService.createSession('chat', baseSessionData, { modelId: params.modelId, message: params.message, enableWebSearch: params.enableWebSearch, temperature: params.temperature, maxTokens: params.maxTokens, conversationHistory: params.conversationHistory }); if (result && typeof result === 'object') { const modelInteractions = this.extractModelInteractions(result, 'chat'); for (const interaction of modelInteractions) { await this.sessionService.addModelInteraction(dbSessionId, 'chat', interaction); } } await this.sessionService.completeSession(dbSessionId, 'chat', processingTime, 'success'); break; case 'list_available_models': case 'get_model_info': // Map method names to schema-compatible endpoints const endpoint = method === 'list_available_models' ? 'list_models' : 'get_model_info'; // Prepare request data only for createSession (following brains pattern) const modelsRequestData = { endpoint, provider: params?.provider, modelId: params?.modelId }; // Create models session with proper schema structure dbSessionId = await this.sessionService.createSession('models', baseSessionData, modelsRequestData); // Now update with response data using the models-specific addModelInteraction const modelsInteractionData = { modelId: 'list_models_response', provider: params?.provider || 'unknown', latency: processingTime, inputTokens: 0, outputTokens: 0, cost: 0, success: result && !result.error, errorMessage: result?.error || undefined, fullResponse: JSON.stringify(result) || '' }; await this.sessionService.addModelInteraction(dbSessionId, 'models', modelsInteractionData); await this.sessionService.completeSession(dbSessionId, 'models', processingTime, 'success'); break; default: logger.warn('Unknown method for database save', { method }); return; } logger.debug('Successfully saved session to database', { sessionId, dbSessionId, method, processingTime, finalTotalCost }); } // 🚀 ENHANCED: Extract model interaction data using rich metadata from mcp_tryaii extractModelInteractions(result, type) { const interactions = []; try { // 🎯 NEW: Direct access to enhanced response structure from mcp_tryaii if (result.responses && Array.isArray(result.responses)) { for (const response of result.responses) { // ✅ DIRECT FIELD ACCESS - No complex parsing needed! interactions.push({ // === CORE MODEL INFO === modelId: response.modelId || response.model?.id || 'unknown', provider: response.provider || response.model?.provider || 'unknown', modelName: response.modelName || response.model?.name || 'unknown', // === TIMING METADATA (Enhanced) === timeStarted: response.timeStarted ? new Date(response.timeStarted) : new Date(), timeCompleted: response.timeCompleted ? new Date(response.timeCompleted) : new Date(), latency: response.actualLatency || response.latency || (response.timeStarted && response.timeCompleted ? new Date(response.timeCompleted).getTime() - new Date(response.timeStarted).getTime() : 0), // 🚀 ENHANCED: Precision timing with fallback calculation // === TOKEN USAGE (Enhanced with thinking tokens) === inputTokens: response.inputTokens || 0, outputTokens: response.outputTokens || 0, thinkingTokens: response.thinkingTokens || 0, // NEW: Reasoning tokens totalTokens: response.totalTokens || (response.inputTokens || 0) + (response.outputTokens || 0) + (response.thinkingTokens || 0), tokensUsed: response.tokensUsed || response.totalTokens || 0, // Legacy field // === COST TRACKING (Enhanced breakdown) === cost: response.cost || 0, inputCost: response.costBreakdown?.inputCost || 0, outputCost: response.costBreakdown?.outputCost || 0, thinkingCost: response.costBreakdown?.thinkingCost || 0, // === REQUEST PARAMETERS (Enhanced) === temperature: response.temperature || 0.7, maxTokens: response.maxTokens || 12000, reasoningEffort: response.reasoningEffort, // === RESPONSE DATA === fullResponse: response.fullResponse || response.response || '', // responsePreview field removed for storage optimization // === STATUS & ERROR HANDLING (Enhanced) === success: response.success !== false && response.status !== 'error', errorMessage: response.error || (response.status === 'error' ? 'Failed' : undefined), errorType: response.errorType, // === FEATURE USAGE (Enhanced) === webSearchEnabled: response.webSearchEnabled || false, webSearchUsed: response.webSearchUsed || false, webSearchResults: response.webSearchResults, reasoningUsed: response.reasoningUsed || false, // === MODEL CAPABILITIES === webSearch: response.modelCapabilities?.webSearch || false, reasoning: response.modelCapabilities?.reasoning || false, maxContextTokens: response.modelCapabilities?.maxContextTokens || 0, latencySpeed: response.modelCapabilities?.latencySpeed || 'unknown', // === METADATA === timestamp: response.timestamp || new Date().toISOString(), requestMetadata: { ...response.metadata, modelProvider: response.provider, enhancedExtraction: true // Flag to indicate this uses enhanced extraction } }); } } // 🔄 FALLBACK: Handle legacy parsing for backward compatibility else if (type === 'brains' && result.content?.[0]?.text) { const responseData = JSON.parse(result.content[0].text); if (responseData.responses && Array.isArray(responseData.responses)) { for (const response of responseData.responses) { // Legacy parsing with limited metadata interactions.push({ modelId: response.model?.id || response.modelId || 'unknown', provider: response.model?.provider || response.provider || 'unknown', modelName: response.model?.name || response.modelName || 'unknown', timeStarted: new Date(), timeCompleted: new Date(), latency: response.latency || 1000, inputTokens: response.inputTokens || Math.floor((response.tokensUsed || 0) * 0.3) || 0, outputTokens: response.outputTokens || response.tokensUsed || 0, thinkingTokens: 0, // Not available in legacy format totalTokens: response.tokensUsed || 0, tokensUsed: response.tokensUsed || 0, cost: response.cost || 0, inputCost: 0, outputCost: response.cost || 0, thinkingCost: 0, temperature: 0.7, maxTokens: 12000, fullResponse: response.response || response.text || response.content || '', // responsePreview field removed for storage optimization success: response.status === 'success', errorMessage: response.error || (response.status !== 'success' ? 'Failed' : undefined), webSearchEnabled: false, webSearchUsed: false, reasoningUsed: false, webSearch: false, reasoning: false, maxContextTokens: 0, latencySpeed: 'unknown', timestamp: response.timestamp || new Date().toISOString(), requestMetadata: { legacyExtraction: true, ...response.metadata } }); } } } // Handle compare_models response format - Multiple models comparison else if (type === 'compare' && result.content?.[0]?.text) { const responseData = JSON.parse(result.content[0].text); if (responseData.results && Array.isArray(responseData.results)) { for (const modelResult of responseData.results) { // Extract actual latency data let actualLatency = 1000; // fallback only if (modelResult.responseTime && modelResult.responseTime > 0) { actualLatency = modelResult.responseTime; } else if (modelResult.latency && modelResult.latency > 0) { actualLatency = modelResult.latency; } else if (modelResult.duration && modelResult.duration > 0) { actualLatency = modelResult.duration; } else if (modelResult.metadata?.latency && modelResult.metadata.latency > 0) { actualLatency = modelResult.metadata.latency; } interactions.push({ modelId: modelResult.modelId || 'unknown', provider: modelResult.provider || 'unknown', inputTokens: modelResult.inputTokens || modelResult.usage?.promptTokens || 0, outputTokens: modelResult.outputTokens || modelResult.usage?.completionTokens || 0, cost: modelResult.cost || 0, latency: actualLatency, success: modelResult.success !== false, errorMessage: modelResult.error || (!modelResult.success ? 'Failed' : undefined), fullResponse: modelResult.response || modelResult.content || '', // responsePreview field removed for storage optimization timestamp: modelResult.timestamp || new Date().toISOString(), requestMetadata: { temperature: modelResult.temperature, maxTokens: modelResult.maxTokens, ...modelResult.metadata } }); } } } // Handle chat_with_model response format - Single model chat else if (type === 'chat' && result.content?.[0]?.text) { const responseData = JSON.parse(result.content[0].text); // Extract actual latency data let actualLatency = 1000; // fallback only if (responseData.responseTime && responseData.responseTime > 0) { actualLatency = responseData.responseTime; } else if (responseData.latency && responseData.latency > 0) { actualLatency = responseData.latency; } else if (responseData.duration && responseData.duration > 0) { actualLatency = responseData.duration; } else if (responseData.metadata?.latency && responseData.metadata.latency > 0) { actualLatency = responseData.metadata.latency; } // Ensure we have all required fields for the model interaction const interaction = { modelId: responseData.modelId || 'unknown', provider: responseData.provider || 'unknown', inputTokens: responseData.inputTokens || responseData.usage?.promptTokens || 0, outputTokens: responseData.outputTokens || responseData.usage?.completionTokens || 0, cost: responseData.cost || 0, latency: actualLatency, success: responseData.success !== false, errorMessage: responseData.error || (!responseData.success ? 'Failed' : undefined), fullResponse: responseData.response || responseData.content || '', // responsePreview field removed for storage optimization timestamp: responseData.timestamp || new Date().toISOString(), requestMetadata: { conversationLength: responseData.conversationHistory?.length || 0, temperature: responseData.temperature, maxTokens: responseData.maxTokens, ...responseData.metadata } }; interactions.push(interaction); } // If no interactions were extracted but we have a result, create a generic interaction if (interactions.length === 0 && result) { logger.warn('Failed to parse model interactions, creating generic interaction', { type, hasContent: !!result.content, resultType: typeof result }); // Create a minimal interaction to prevent validation errors interactions.push({ modelId: 'unknown', provider: 'unknown', modelName: 'unknown', timeStarted: new Date(), timeCompleted: new Date(), inputTokens: 0, outputTokens: 0, thinkingTokens: 0, totalTokens: 0, tokensUsed: 0, cost: 0, inputCost: 0, outputCost: 0, thinkingCost: 0, temperature: 0.7, maxTokens: 12000, latency: 1000, // Keep fallback for when we truly don't have data success: !!result.content, errorMessage: result.error || (!result.content ? 'No response content' : undefined), fullResponse: JSON.stringify(result), // responsePreview field removed for storage optimization timestamp: new Date().toISOString(), webSearchEnabled: false, webSearchUsed: false, reasoningUsed: false, webSearch: false, reasoning: false, maxContextTokens: 0, latencySpeed: 'unknown', requestMetadata: {} }); } // Log successful extraction if (interactions.length > 0) { const enhancedCount = interactions.filter(i => i.requestMetadata?.enhancedExtraction).length; const legacyCount = interactions.filter(i => i.requestMetadata?.legacyExtraction).length; logger.info('🚀 Enhanced model interactions extracted', { type, interactionCount: interactions.length, enhancedExtractions: enhancedCount, legacyExtractions: legacyCount, totalCost: interactions.reduce((sum, i) => sum + (i.cost || 0), 0), totalTokens: interactions.reduce((sum, i) => sum + (i.totalTokens || 0), 0), totalThinkingTokens: interactions.reduce((sum, i) => sum + (i.thinkingTokens || 0), 0), totalLatency: interactions.reduce((sum, i) => sum + (i.latency || 0), 0), averageLatency: interactions.length > 0 ? interactions.reduce((sum, i) => sum + (i.latency || 0), 0) / interactions.length : 0, successCount: interactions.filter(i => i.success).length, reasoningUsageCount: interactions.filter(i => i.reasoningUsed).length, webSearchUsageCount: interactions.filter(i => i.webSearchUsed).length }); } } catch (error) { logger.warn('Failed to extract model interactions', { error, type, resultType: typeof result }); // Create a fallback interaction to prevent validation errors interactions.push({ modelId: 'unknown', provider: 'unknown', modelName: 'unknown', timeStarted: new Date(), timeCompleted: new Date(), inputTokens: 0, outputTokens: 0, thinkingTokens: 0, totalTokens: 0, tokensUsed: 0, cost: 0, inputCost: 0, outputCost: 0, thinkingCost: 0, temperature: 0, maxTokens: 0, latency: 1000, success: false, errorMessage: `Parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, fullResponse: '', // responsePreview field removed for storage optimization timestamp: new Date().toISOString(), webSearchEnabled: false, webSearchUsed: false, reasoningUsed: false, webSearch: false, reasoning: false, maxContextTokens: 0, latencySpeed: 'unknown', requestMetadata: {} }); } return interactions; } } // Create singleton instance export const sessionManager = new SessionManager(); //# sourceMappingURL=sessionManager.js.map