const { getModelPricing } = require('../config/pricing'); // Cache agents collection in memory, refresh every 5 min let agentsCache = new Map(); let agentsCacheTime = 0; const CACHE_TTL = 5 * 60 * 1000; async function refreshAgentsCache(db) { if (Date.now() - agentsCacheTime < CACHE_TTL && agentsCache.size > 0) return; try { const agents = await db.collection('agents').find({}).toArray(); agentsCache = new Map(); for (const a of agents) { agentsCache.set(a.id, { name: a.name, model: a.model, provider: a.provider }); } agentsCacheTime = Date.now(); } catch (e) { console.error('Failed to refresh agents cache:', e.message); } } function resolveModel(model) { if (model && model.startsWith('agent_')) { const agent = agentsCache.get(model); return agent ? agent.model : model; } return model; } function getDateRange(query) { const { period, start, end } = query; const now = new Date(); let startDate, endDate; if (period === 'custom' && start && end) { startDate = new Date(start); endDate = new Date(end); endDate.setHours(23, 59, 59, 999); } else { endDate = now; switch (period) { case '7d': startDate = new Date(now - 7 * 86400000); break; case '30d': startDate = new Date(now - 30 * 86400000); break; case '24h': default: startDate = new Date(now - 24 * 3600000); break; } } return { startDate, endDate }; } async function getSummary(db, query) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); const [tokenResult, userCount, convCount, visits, uniqueUsers] = await Promise.all([ db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: null, totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, } } ]).toArray(), db.collection('transactions').distinct('user', { createdAt: { $gte: startDate, $lte: endDate } }), db.collection('transactions').distinct('conversationId', { createdAt: { $gte: startDate, $lte: endDate } }), db.collection('conversations').countDocuments({ createdAt: { $gte: startDate, $lte: endDate } }), db.collection('conversations').distinct('user', { createdAt: { $gte: startDate, $lte: endDate } }), ]); const t = tokenResult[0] || { totalTokens: 0, totalCost: 0 }; return { totalTokens: t.totalTokens, totalCost: t.totalCost / 1_000_000, activeUsers: userCount.length, conversations: convCount.length, visits: visits, uniqueUsers: uniqueUsers.length, }; } async function getTopUsers(db, query, limit = 10) { const { startDate, endDate } = getDateRange(query); const results = await db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: '$user', totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, conversations: { $addToSet: '$conversationId' }, } }, { $sort: { totalCost: -1 } }, { $limit: limit }, { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'userInfo' } }, { $unwind: { path: '$userInfo', preserveNullAndEmptyArrays: true } }, { $project: { name: { $ifNull: ['$userInfo.name', 'Unknown'] }, email: { $ifNull: ['$userInfo.email', ''] }, totalTokens: 1, totalCost: { $divide: ['$totalCost', 1_000_000] }, conversationCount: { $size: '$conversations' }, } } ]).toArray(); return results; } async function getTopModels(db, query, limit = 10) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); const raw = await db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: { model: '$model', tokenType: '$tokenType' }, totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, } } ]).toArray(); // Resolve agents to underlying LLM and re-aggregate const modelMap = new Map(); for (const r of raw) { const resolvedModel = resolveModel(r._id.model); const key = `${resolvedModel}::${r._id.tokenType}`; if (!modelMap.has(key)) { modelMap.set(key, { model: resolvedModel, tokenType: r._id.tokenType, totalTokens: 0, totalCost: 0 }); } const entry = modelMap.get(key); entry.totalTokens += r.totalTokens; entry.totalCost += r.totalCost; } // Pivot into per-model summary const models = new Map(); for (const entry of modelMap.values()) { if (!models.has(entry.model)) { models.set(entry.model, { model: entry.model, promptTokens: 0, completionTokens: 0, promptCost: 0, completionCost: 0, totalCost: 0 }); } const m = models.get(entry.model); if (entry.tokenType === 'prompt') { m.promptTokens += entry.totalTokens; m.promptCost += entry.totalCost / 1_000_000; } else { m.completionTokens += entry.totalTokens; m.completionCost += entry.totalCost / 1_000_000; } m.totalCost = m.promptCost + m.completionCost; } return Array.from(models.values()) .sort((a, b) => b.totalCost - a.totalCost) .slice(0, limit); } async function getTopAgents(db, query, limit = 10) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); // LibreChat changed transaction shape around 2026-03: agent transactions used // to record `model: agent_xxx` directly; newer ones record the underlying LLM // (e.g. gpt-5.2) and link back to a message via `messageId`. Sum across both. const agentMsgs = await db.collection('messages').find( { createdAt: { $gte: startDate, $lte: endDate }, model: { $regex: /^agent_/ } }, { projection: { messageId: 1, model: 1, conversationId: 1 } } ).toArray(); const msgToAgent = new Map(agentMsgs.map(m => [m.messageId, m.model])); const newPath = msgToAgent.size === 0 ? [] : await db.collection('transactions').aggregate([ { $match: { messageId: { $in: [...msgToAgent.keys()] }, createdAt: { $gte: startDate, $lte: endDate }, } }, { $group: { _id: '$messageId', totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, conversationId: { $first: '$conversationId' }, } } ]).toArray(); const legacy = await db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate }, model: { $regex: /^agent_/ } } }, { $group: { _id: '$model', totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, conversations: { $addToSet: '$conversationId' }, } } ]).toArray(); const totals = new Map(); const bump = (agentId, tokens, cost, convs) => { if (!agentId) return; if (!totals.has(agentId)) { totals.set(agentId, { agentId, totalTokens: 0, totalCost: 0, convs: new Set() }); } const a = totals.get(agentId); a.totalTokens += tokens; a.totalCost += cost; for (const c of convs) if (c) a.convs.add(c); }; for (const t of newPath) bump(msgToAgent.get(t._id), t.totalTokens, t.totalCost, [t.conversationId]); for (const t of legacy) bump(t._id, t.totalTokens, t.totalCost, t.conversations); return [...totals.values()] .sort((a, b) => b.totalCost - a.totalCost) .slice(0, limit) .map(a => { const meta = agentsCache.get(a.agentId); return { agentId: a.agentId, totalTokens: a.totalTokens, totalCost: a.totalCost / 1_000_000, conversationCount: a.convs.size, agentName: meta ? meta.name : a.agentId, underlyingModel: meta ? meta.model : 'Unknown', provider: meta ? meta.provider : 'Unknown', }; }); } async function getCostBreakdown(db, query) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); const raw = await db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: { model: '$model', tokenType: '$tokenType' }, totalCost: { $sum: { $abs: '$tokenValue' } }, } } ]).toArray(); const models = new Map(); for (const r of raw) { const resolved = resolveModel(r._id.model); if (!models.has(resolved)) { models.set(resolved, { model: resolved, inputCost: 0, outputCost: 0 }); } const m = models.get(resolved); if (r._id.tokenType === 'prompt') { m.inputCost += r.totalCost / 1_000_000; } else { m.outputCost += r.totalCost / 1_000_000; } } return Array.from(models.values()) .map(m => ({ ...m, totalCost: m.inputCost + m.outputCost })) .sort((a, b) => b.totalCost - a.totalCost); } async function getUsageOverTime(db, query) { const { startDate, endDate } = getDateRange(query); const diffMs = endDate - startDate; const diffHours = diffMs / 3600000; // Use hourly buckets for <=48h, daily for longer let dateFormat, bucketLabel; if (diffHours <= 48) { dateFormat = { $dateToString: { format: '%Y-%m-%dT%H:00', date: '$createdAt' } }; bucketLabel = 'hour'; } else { dateFormat = { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }; bucketLabel = 'day'; } const results = await db.collection('transactions').aggregate([ { $match: { createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: dateFormat, totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, } }, { $sort: { _id: 1 } }, ]).toArray(); return { bucketType: bucketLabel, data: results.map(r => ({ time: r._id, tokens: r.totalTokens, cost: r.totalCost / 1_000_000, })), }; } async function getTopConversations(db, query, limit = 20, userId = null) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); const match = { createdAt: { $gte: startDate, $lte: endDate } }; if (userId) match.user = new (require('mongodb').ObjectId)(userId); const results = await db.collection('transactions').aggregate([ { $match: match }, { $group: { _id: '$conversationId', totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, models: { $addToSet: '$model' }, user: { $first: '$user' }, promptTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'prompt'] }, { $abs: '$rawAmount' }, 0] } }, completionTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'completion'] }, { $abs: '$rawAmount' }, 0] } }, } }, { $sort: { totalCost: -1 } }, { $limit: limit }, { $lookup: { from: 'users', localField: 'user', foreignField: '_id', as: 'userInfo' } }, { $unwind: { path: '$userInfo', preserveNullAndEmptyArrays: true } }, { $lookup: { from: 'conversations', localField: '_id', foreignField: 'conversationId', as: 'convInfo' } }, { $unwind: { path: '$convInfo', preserveNullAndEmptyArrays: true } }, { $project: { conversationId: '$_id', title: { $ifNull: ['$convInfo.title', 'Unknown'] }, userName: { $ifNull: ['$userInfo.name', 'Unknown'] }, userEmail: { $ifNull: ['$userInfo.email', ''] }, models: 1, totalTokens: 1, promptTokens: 1, completionTokens: 1, totalCost: { $divide: ['$totalCost', 1_000_000] }, } } ]).toArray(); // Resolve agent model names return results.map(r => ({ ...r, models: r.models.map(m => { if (m && m.startsWith('agent_')) { const agent = agentsCache.get(m); return agent ? `${agent.name} (${agent.model})` : m; } return m; }), })); } async function searchUsers(db, searchTerm, limit = 10) { const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); return db.collection('users').find({ $or: [{ name: regex }, { email: regex }] }, { projection: { _id: 1, name: 1, email: 1 } }) .limit(limit) .toArray(); } async function getUserDetail(db, query, userId) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); const { ObjectId } = require('mongodb'); const userOid = new ObjectId(userId); // Summary aggregation const [summaryResult, visitCount, convos, modelBreakdown] = await Promise.all([ db.collection('transactions').aggregate([ { $match: { user: userOid, createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: null, totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, promptTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'prompt'] }, { $abs: '$rawAmount' }, 0] } }, completionTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'completion'] }, { $abs: '$rawAmount' }, 0] } }, conversations: { $addToSet: '$conversationId' }, } } ]).toArray(), db.collection('conversations').countDocuments({ user: userId, createdAt: { $gte: startDate, $lte: endDate } }), // Conversations breakdown db.collection('transactions').aggregate([ { $match: { user: userOid, createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: '$conversationId', totalCost: { $sum: { $abs: '$tokenValue' } }, models: { $addToSet: '$model' }, promptTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'prompt'] }, { $abs: '$rawAmount' }, 0] } }, completionTokens: { $sum: { $cond: [{ $eq: ['$tokenType', 'completion'] }, { $abs: '$rawAmount' }, 0] } }, } }, { $sort: { totalCost: -1 } }, { $limit: 100 }, { $lookup: { from: 'conversations', localField: '_id', foreignField: 'conversationId', as: 'convInfo' } }, { $unwind: { path: '$convInfo', preserveNullAndEmptyArrays: true } }, { $project: { conversationId: '$_id', title: { $ifNull: ['$convInfo.title', 'Unknown'] }, models: 1, promptTokens: 1, completionTokens: 1, totalCost: { $divide: ['$totalCost', 1_000_000] }, } } ]).toArray(), // Model breakdown db.collection('transactions').aggregate([ { $match: { user: userOid, createdAt: { $gte: startDate, $lte: endDate } } }, { $group: { _id: { model: '$model', tokenType: '$tokenType' }, totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, } } ]).toArray(), ]); const s = summaryResult[0] || { totalTokens: 0, totalCost: 0, promptTokens: 0, completionTokens: 0, conversations: [] }; // Resolve agent models in conversations const resolvedConvos = convos.map(r => ({ ...r, models: r.models.map(m => { if (m && m.startsWith('agent_')) { const agent = agentsCache.get(m); return agent ? `${agent.name} (${agent.model})` : m; } return m; }), })); // Pivot model breakdown and resolve agents const models = new Map(); for (const entry of modelBreakdown) { const resolvedModel = resolveModel(entry._id.model); if (!models.has(resolvedModel)) { models.set(resolvedModel, { model: resolvedModel, promptTokens: 0, completionTokens: 0, promptCost: 0, completionCost: 0, totalCost: 0 }); } const m = models.get(resolvedModel); if (entry._id.tokenType === 'prompt') { m.promptTokens += entry.totalTokens; m.promptCost += entry.totalCost / 1_000_000; } else { m.completionTokens += entry.totalTokens; m.completionCost += entry.totalCost / 1_000_000; } m.totalCost = m.promptCost + m.completionCost; } return { summary: { totalCost: s.totalCost / 1_000_000, totalTokens: s.totalTokens, promptTokens: s.promptTokens, completionTokens: s.completionTokens, conversationCount: s.conversations.length, visitCount: visitCount, }, conversations: resolvedConvos, models: Array.from(models.values()).sort((a, b) => b.totalCost - a.totalCost), }; } module.exports = { getSummary, getTopUsers, getTopModels, getTopAgents, getCostBreakdown, getUsageOverTime, getTopConversations, searchUsers, getUserDetail, };