tryaii-mcp-server
Version:
TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence
964 lines • 47.2 kB
JavaScript
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