diff --git a/server.js b/server.js index 0292438..616ce54 100644 --- a/server.js +++ b/server.js @@ -15,9 +15,23 @@ async function connectDB() { await client.connect(); db = client.db(); console.log('Connected to MongoDB'); + await ensureIndexes(db); return db; } +async function ensureIndexes(db) { + try { + await Promise.all([ + db.collection('transactions').createIndex({ createdAt: -1 }), + db.collection('transactions').createIndex({ messageId: 1 }), + db.collection('messages').createIndex({ createdAt: -1, model: 1 }), + ]); + console.log('Indexes ensured'); + } catch (e) { + console.error('Index creation failed:', e.message); + } +} + // Health check (no auth) app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); diff --git a/services/analytics.js b/services/analytics.js index 1139ce4..66205cb 100644 --- a/services/analytics.js +++ b/services/analytics.js @@ -180,37 +180,68 @@ async function getTopAgents(db, query, limit = 10) { await refreshAgentsCache(db); const { startDate, endDate } = getDateRange(query); - const results = await db.collection('transactions').aggregate([ + // 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: { + { $group: { _id: '$model', totalTokens: { $sum: { $abs: '$rawAmount' } }, totalCost: { $sum: { $abs: '$tokenValue' } }, conversations: { $addToSet: '$conversationId' }, - } - }, - { $sort: { totalCost: -1 } }, - { $limit: limit }, - { - $project: { - agentId: '$_id', - totalTokens: 1, - totalCost: { $divide: ['$totalCost', 1_000_000] }, - conversationCount: { $size: '$conversations' }, - } - } + } } ]).toArray(); - return results.map(r => { - const agent = agentsCache.get(r.agentId); - return { - ...r, - agentName: agent ? agent.name : r.agentId, - underlyingModel: agent ? agent.model : 'Unknown', - provider: agent ? agent.provider : 'Unknown', - }; - }); + 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) {