mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
Backend: GET /api/usage (requireJwtAuth) aggregates this user's prompt+ completion Transaction spend over today/7d/30d plus a per-model breakdown, enriched with the org tier from CommerceClient. Response mirrors the @hanzo/usage UsageSnapshot shape (providerId 'hanzo', totals, providerCost) for one wire format across Hanzo products. Frontend: new Usage settings tab beside Balance, reusing the UsageBar idiom; useGetUserUsage hook follows useGetUserBalance; en localization strings added.
94 lines
3.1 KiB
JavaScript
94 lines
3.1 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const { Transaction } = require('~/db/models');
|
|
const { getCommerceClient } = require('~/server/services/CommerceClient');
|
|
|
|
/** 1e6 tokenCredits == $1 USD (mirrors client creditsToUsd). */
|
|
const CREDITS_PER_USD = 1_000_000;
|
|
/** Only prompt/completion transactions represent AI usage (spend); `credits` are deposits. */
|
|
const SPEND_TYPES = ['prompt', 'completion'];
|
|
|
|
/** One accumulator over abs(rawAmount) tokens, abs(tokenValue) credits, and request count. */
|
|
const spendGroup = (id) => ({
|
|
$group: {
|
|
_id: id,
|
|
tokens: { $sum: { $abs: { $ifNull: ['$rawAmount', 0] } } },
|
|
credits: { $sum: { $abs: { $ifNull: ['$tokenValue', 0] } } },
|
|
requests: { $sum: 1 },
|
|
},
|
|
});
|
|
|
|
/** Shape a facet bucket into the @hanzo/usage window schema (UsageTotals + ProviderCostSnapshot). */
|
|
function toWindow(bucket) {
|
|
const row = (bucket && bucket[0]) || { tokens: 0, credits: 0, requests: 0 };
|
|
return {
|
|
totals: { tokens: row.tokens, requests: row.requests },
|
|
providerCost: { used: row.credits / CREDITS_PER_USD, currencyCode: 'USD' },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Unified per-user AI usage. Aggregates this user's spend (prompt + completion
|
|
* Transactions) over today / 7d / 30d plus a per-model breakdown, enriched with
|
|
* the org tier from Commerce. Response mirrors @hanzo/usage's UsageSnapshot
|
|
* shape (providerId 'hanzo', totals, providerCost) so it is one wire format
|
|
* across Hanzo products.
|
|
*/
|
|
async function usageController(req, res) {
|
|
const userId = new mongoose.Types.ObjectId(req.user.id);
|
|
const now = new Date();
|
|
const since30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
const since7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
const [facet] = await Transaction.aggregate([
|
|
{ $match: { user: userId, tokenType: { $in: SPEND_TYPES }, createdAt: { $gte: since30 } } },
|
|
{
|
|
$facet: {
|
|
today: [{ $match: { createdAt: { $gte: startOfToday } } }, spendGroup(null)],
|
|
d7: [{ $match: { createdAt: { $gte: since7 } } }, spendGroup(null)],
|
|
d30: [spendGroup(null)],
|
|
models: [spendGroup('$model'), { $sort: { credits: -1 } }, { $limit: 20 }],
|
|
},
|
|
},
|
|
]);
|
|
|
|
const models = (facet?.models ?? [])
|
|
.filter((m) => m._id)
|
|
.map((m) => ({
|
|
model: m._id,
|
|
tokens: m.tokens,
|
|
requests: m.requests,
|
|
cost: m.credits / CREDITS_PER_USD,
|
|
}));
|
|
|
|
const payload = {
|
|
providerId: 'hanzo',
|
|
currencyCode: 'USD',
|
|
tier: 'Free',
|
|
windows: {
|
|
today: toWindow(facet?.today),
|
|
'7d': toWindow(facet?.d7),
|
|
'30d': toWindow(facet?.d30),
|
|
},
|
|
models,
|
|
updatedAt: now.toISOString(),
|
|
};
|
|
|
|
// Enrich with Commerce tier (fail-open: usage is still authoritative locally).
|
|
const commerceClient = getCommerceClient();
|
|
if (commerceClient) {
|
|
try {
|
|
const tier = await commerceClient.getTierConfig(req.user.id);
|
|
if (tier) {
|
|
payload.tier = tier.displayName || tier.name || payload.tier;
|
|
}
|
|
} catch {
|
|
// ignore — return local usage without tier enrichment
|
|
}
|
|
}
|
|
|
|
res.status(200).json(payload);
|
|
}
|
|
|
|
module.exports = usageController;
|