fix: Repair Top Agents tab after LibreChat transaction shape change
LibreChat stopped tagging agent transactions with model: agent_xxx around 2026-03; new agent transactions record the underlying LLM and link to the message via messageId. Aggregate from messages -> transactions and union with the legacy path so historical and current data both show. Also create createdAt / messageId / (createdAt, model) indexes on startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1673694b11
commit
31358a8b86
2 changed files with 69 additions and 24 deletions
14
server.js
14
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() });
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue