@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
317 lines • 10.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageTracker = void 0;
const events_1 = require("events");
const UsageStorage_1 = require("./UsageStorage");
const MetricsCollector_1 = require("./MetricsCollector");
const log_1 = require("../utils/log");
/**
* Usage tracking system for Conduit
*/
class UsageTracker extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.isInitialized = false;
this.config = {
enabled: true,
storage: {
type: 'memory',
retention: 30
},
metrics: {
enabled: false
},
...config
};
this.storage = new UsageStorage_1.UsageStorage(this.config.storage || {});
this.metrics = new MetricsCollector_1.MetricsCollector(this.config.metrics || {});
}
/**
* Initialize usage tracking
*/
async initialize() {
if (this.isInitialized) {
return;
}
if (!this.config.enabled) {
(0, log_1.log)('Usage tracking is disabled');
return;
}
(0, log_1.log)('Initializing usage tracking...');
await this.storage.initialize();
await this.metrics.initialize();
this.isInitialized = true;
this.emit('initialized');
(0, log_1.log)('Usage tracking initialized');
}
/**
* Track usage data
*/
async trackUsage(usage) {
if (!this.isInitialized || !this.config.enabled) {
return;
}
try {
// Validate usage data
if (!this.validateUsageData(usage)) {
throw new Error('Invalid usage data');
}
// Store usage data
await this.storage.store(usage);
// Collect metrics
if (this.config.metrics?.enabled) {
await this.metrics.record(usage);
}
this.emit('usage-tracked', usage);
}
catch (error) {
(0, log_1.log)('Error tracking usage:', error);
this.emit('error', error);
throw error;
}
}
/**
* Get usage statistics
*/
async getUsageStats(query = {}) {
if (!this.isInitialized) {
throw new Error('Usage tracker not initialized');
}
try {
const usageData = await this.storage.query(query);
return this.calculateStats(usageData, query);
}
catch (error) {
(0, log_1.log)('Error getting usage stats:', error);
this.emit('error', error);
throw error;
}
}
/**
* Get cost breakdown
*/
async getCostBreakdown(query = {}) {
if (!this.isInitialized) {
throw new Error('Usage tracker not initialized');
}
try {
const usageData = await this.storage.query(query);
return this.calculateCostBreakdown(usageData);
}
catch (error) {
(0, log_1.log)('Error getting cost breakdown:', error);
this.emit('error', error);
throw error;
}
}
/**
* Get recent usage data
*/
async getRecentUsage(limit = 100) {
if (!this.isInitialized) {
throw new Error('Usage tracker not initialized');
}
return this.storage.query({ limit });
}
/**
* Clear old usage data
*/
async cleanupOldData(olderThanDays = 30) {
if (!this.isInitialized) {
return;
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
await this.storage.cleanup(cutoffDate);
this.emit('cleanup', { cutoffDate, olderThanDays });
}
/**
* Export usage data
*/
async exportData(query = {}, format = 'json') {
if (!this.isInitialized) {
throw new Error('Usage tracker not initialized');
}
const data = await this.storage.query(query);
if (format === 'csv') {
return this.formatAsCSV(data);
}
return JSON.stringify(data, null, 2);
}
/**
* Get usage tracking status
*/
getStatus() {
return {
initialized: this.isInitialized,
enabled: this.config.enabled,
storageType: this.config.storage?.type,
metricsEnabled: this.config.metrics?.enabled
};
}
/**
* Update configuration
*/
async updateConfig(config) {
this.config = { ...this.config, ...config };
if (this.isInitialized) {
await this.storage.updateConfig(this.config.storage || {});
await this.metrics.updateConfig(this.config.metrics || {});
}
this.emit('config-updated', this.config);
}
/**
* Validate usage data
*/
validateUsageData(usage) {
return !!(usage.id &&
usage.timestamp &&
usage.model &&
typeof usage.tokenCount === 'number' &&
usage.routingReason);
}
/**
* Calculate statistics from usage data
*/
calculateStats(usageData, query) {
const stats = {
totalRequests: usageData.length,
totalTokens: 0,
totalCost: 0,
averageTokensPerRequest: 0,
modelBreakdown: {},
timeRange: {
start: query.timeRange?.start || new Date(0),
end: query.timeRange?.end || new Date()
},
projectBreakdown: {},
agentBreakdown: {}
};
// Calculate totals and breakdowns
for (const usage of usageData) {
stats.totalTokens += usage.tokenCount;
stats.totalCost += usage.cost || 0;
// Model breakdown
if (!stats.modelBreakdown[usage.model]) {
stats.modelBreakdown[usage.model] = {
requests: 0,
tokens: 0,
cost: 0
};
}
stats.modelBreakdown[usage.model].requests++;
stats.modelBreakdown[usage.model].tokens += usage.tokenCount;
stats.modelBreakdown[usage.model].cost += usage.cost || 0;
// Project breakdown
const projectId = usage.projectId;
if (projectId) {
if (!stats.projectBreakdown[projectId]) {
stats.projectBreakdown[projectId] = {
requests: 0,
tokens: 0,
cost: 0
};
}
stats.projectBreakdown[projectId].requests++;
stats.projectBreakdown[projectId].tokens += usage.tokenCount;
stats.projectBreakdown[projectId].cost += usage.cost || 0;
}
// Agent breakdown
const agentId = usage.agentId;
if (agentId) {
if (!stats.agentBreakdown[agentId]) {
stats.agentBreakdown[agentId] = {
requests: 0,
tokens: 0,
cost: 0
};
}
stats.agentBreakdown[agentId].requests++;
stats.agentBreakdown[agentId].tokens += usage.tokenCount;
stats.agentBreakdown[agentId].cost += usage.cost || 0;
}
}
// Calculate averages
stats.averageTokensPerRequest = stats.totalRequests > 0
? Math.round(stats.totalTokens / stats.totalRequests)
: 0;
return stats;
}
/**
* Calculate cost breakdown
*/
calculateCostBreakdown(usageData) {
const breakdown = {
total: 0,
byModel: {},
byProject: {},
byAgent: {},
byTimeRange: []
};
// Group by date for time range breakdown
const dailyCosts = {};
for (const usage of usageData) {
const cost = usage.cost || 0;
breakdown.total += cost;
// By model
if (!breakdown.byModel[usage.model]) {
breakdown.byModel[usage.model] = 0;
}
breakdown.byModel[usage.model] += cost;
// By project
const projectId = usage.projectId;
if (projectId) {
if (!breakdown.byProject[projectId]) {
breakdown.byProject[projectId] = 0;
}
breakdown.byProject[projectId] += cost;
}
// By agent
const agentId = usage.agentId;
if (agentId) {
if (!breakdown.byAgent[agentId]) {
breakdown.byAgent[agentId] = 0;
}
breakdown.byAgent[agentId] += cost;
}
// By date
const date = usage.timestamp.toISOString().split('T')[0];
if (!dailyCosts[date]) {
dailyCosts[date] = 0;
}
dailyCosts[date] += cost;
}
// Convert daily costs to array
breakdown.byTimeRange = Object.entries(dailyCosts)
.map(([date, cost]) => ({ date, cost }))
.sort((a, b) => a.date.localeCompare(b.date));
return breakdown;
}
/**
* Format data as CSV
*/
formatAsCSV(data) {
if (data.length === 0) {
return '';
}
const headers = Object.keys(data[0]);
const rows = data.map(item => headers.map(header => JSON.stringify(item[header] || '')).join(','));
return [headers.join(','), ...rows].join('\n');
}
/**
* Cleanup resources
*/
async cleanup() {
if (!this.isInitialized) {
return;
}
(0, log_1.log)('Cleaning up usage tracker...');
await this.storage.cleanup();
await this.metrics.cleanup();
this.isInitialized = false;
this.emit('cleanup');
(0, log_1.log)('Usage tracker cleaned up');
}
}
exports.UsageTracker = UsageTracker;
//# sourceMappingURL=UsageTracker.js.map