cost-claude
Version:
Claude Code cost monitoring, analytics, and optimization toolkit
235 lines • 10.4 kB
JavaScript
import { logger } from '../utils/logger.js';
import { JSONLParser } from '../core/jsonl-parser.js';
export class CostPredictor {
messages = [];
minDataDays = 3;
async loadHistoricalData(projectPath, days = 30) {
try {
const parser = new JSONLParser();
const allMessages = await parser.parseDirectory(projectPath);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
this.messages = allMessages.filter((msg) => msg.timestamp && new Date(msg.timestamp) >= cutoffDate && msg.costUSD);
logger.info(`Loaded ${this.messages.length} messages for prediction analysis`);
}
catch (error) {
logger.error('Error loading historical data:', error);
throw error;
}
}
analyzeUsagePattern() {
if (this.messages.length === 0) {
logger.warn('No messages available for pattern analysis');
return null;
}
const hourCounts = new Array(24).fill(0);
const dayCounts = new Array(7).fill(0);
const hourCosts = new Array(24).fill(0);
const dayCosts = new Array(7).fill(0);
const sessionCosts = new Map();
const dailySessions = new Map();
let totalInputTokens = 0;
let totalOutputTokens = 0;
let tokenMessageCount = 0;
this.messages.forEach(msg => {
if (!msg.timestamp || !msg.costUSD)
return;
const date = new Date(msg.timestamp);
const hour = date.getHours();
const day = date.getDay();
const dateStr = date.toDateString();
hourCounts[hour]++;
dayCounts[day]++;
hourCosts[hour] += msg.costUSD;
dayCosts[day] += msg.costUSD;
if (msg.sessionId) {
sessionCosts.set(msg.sessionId, (sessionCosts.get(msg.sessionId) || 0) + msg.costUSD);
if (!dailySessions.has(dateStr)) {
dailySessions.set(dateStr, new Set());
}
dailySessions.get(dateStr).add(msg.sessionId);
}
if (msg.message && typeof msg.message === 'object' && msg.message.usage) {
totalInputTokens += msg.message.usage.input_tokens || 0;
totalOutputTokens += msg.message.usage.output_tokens || 0;
tokenMessageCount++;
}
});
const totalDays = dailySessions.size || 1;
const totalSessions = sessionCosts.size || 1;
const averageCostPerSession = Array.from(sessionCosts.values())
.reduce((sum, cost) => sum + cost, 0) / totalSessions;
const averageSessionsPerDay = Array.from(dailySessions.values())
.reduce((sum, sessions) => sum + sessions.size, 0) / totalDays;
const trend = this.calculateTrend();
return {
hourOfDay: hourCosts.map((cost, hour) => hourCounts[hour] > 0 ? cost / hourCounts[hour] : 0),
dayOfWeek: dayCosts.map((cost, day) => dayCounts[day] > 0 ? cost / dayCounts[day] : 0),
averageCostPerSession,
averageSessionsPerDay,
averageTokensPerMessage: {
input: tokenMessageCount > 0 ? totalInputTokens / tokenMessageCount : 0,
output: tokenMessageCount > 0 ? totalOutputTokens / tokenMessageCount : 0
},
trend: trend.direction,
trendPercentage: trend.percentage
};
}
calculateTrend() {
if (this.messages.length < this.minDataDays * 10) {
return { direction: 'stable', percentage: 0 };
}
const dailyCosts = new Map();
this.messages.forEach(msg => {
if (!msg.timestamp || !msg.costUSD)
return;
const dateStr = new Date(msg.timestamp).toDateString();
dailyCosts.set(dateStr, (dailyCosts.get(dateStr) || 0) + msg.costUSD);
});
const sortedDays = Array.from(dailyCosts.entries())
.sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime());
if (sortedDays.length < 2) {
return { direction: 'stable', percentage: 0 };
}
const n = sortedDays.length;
const halfN = Math.floor(n / 2);
const firstHalfAvg = sortedDays.slice(0, halfN)
.reduce((sum, [_, cost]) => sum + cost, 0) / halfN;
const secondHalfAvg = sortedDays.slice(halfN)
.reduce((sum, [_, cost]) => sum + cost, 0) / (n - halfN);
const changePercent = ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100;
if (Math.abs(changePercent) < 5) {
return { direction: 'stable', percentage: 0 };
}
else if (changePercent > 0) {
return { direction: 'increasing', percentage: changePercent };
}
else {
return { direction: 'decreasing', percentage: Math.abs(changePercent) };
}
}
predict() {
const pattern = this.analyzeUsagePattern();
if (!pattern) {
return null;
}
const uniqueDays = new Set(this.messages
.filter(msg => msg.timestamp)
.map(msg => new Date(msg.timestamp).toDateString()));
const daysOfData = uniqueDays.size;
if (daysOfData < this.minDataDays) {
logger.warn(`Insufficient data for prediction. Need at least ${this.minDataDays} days, have ${daysOfData}`);
return null;
}
const totalCost = this.messages.reduce((sum, msg) => sum + (msg.costUSD || 0), 0);
const avgDailyCost = totalCost / daysOfData;
let trendMultiplier = 1;
if (pattern.trend === 'increasing') {
trendMultiplier = 1 + (pattern.trendPercentage / 100) * 0.1;
}
else if (pattern.trend === 'decreasing') {
trendMultiplier = 1 - (pattern.trendPercentage / 100) * 0.1;
}
const nextDay = avgDailyCost * trendMultiplier;
const nextWeek = nextDay * 7;
const nextMonth = nextDay * 30;
const confidence = this.calculateConfidence(daysOfData, pattern);
return {
nextDay,
nextWeek,
nextMonth,
confidence,
basedOnDays: daysOfData,
pattern
};
}
calculateConfidence(daysOfData, pattern) {
let confidence = 0;
if (daysOfData >= 30)
confidence += 40;
else if (daysOfData >= 14)
confidence += 30;
else if (daysOfData >= 7)
confidence += 20;
else
confidence += 10;
if (pattern.trend === 'stable')
confidence += 30;
else if (pattern.trendPercentage < 20)
confidence += 20;
else if (pattern.trendPercentage < 50)
confidence += 10;
const variance = this.calculateVariance();
if (variance < 0.2)
confidence += 30;
else if (variance < 0.5)
confidence += 20;
else if (variance < 1.0)
confidence += 10;
return Math.min(confidence, 95);
}
calculateVariance() {
const dailyCosts = new Map();
this.messages.forEach(msg => {
if (!msg.timestamp || !msg.costUSD)
return;
const dateStr = new Date(msg.timestamp).toDateString();
dailyCosts.set(dateStr, (dailyCosts.get(dateStr) || 0) + msg.costUSD);
});
const costs = Array.from(dailyCosts.values());
if (costs.length === 0)
return 1;
const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length;
const variance = costs.reduce((sum, cost) => sum + Math.pow(cost - mean, 2), 0) / costs.length;
return variance / (mean * mean);
}
generateInsights(prediction) {
const insights = [];
if (prediction.pattern.trend === 'increasing') {
insights.push(`📈 Your usage is trending up by ${prediction.pattern.trendPercentage.toFixed(1)}%`);
}
else if (prediction.pattern.trend === 'decreasing') {
insights.push(`📉 Your usage is trending down by ${prediction.pattern.trendPercentage.toFixed(1)}%`);
}
else {
insights.push('📊 Your usage pattern is stable');
}
const peakHour = prediction.pattern.hourOfDay.indexOf(Math.max(...prediction.pattern.hourOfDay));
const peakDay = prediction.pattern.dayOfWeek.indexOf(Math.max(...prediction.pattern.dayOfWeek));
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
insights.push(`🕐 Peak usage: ${peakHour}:00-${peakHour + 1}:00 on ${days[peakDay]}s`);
insights.push(`💬 Average ${prediction.pattern.averageSessionsPerDay.toFixed(1)} sessions/day at $${prediction.pattern.averageCostPerSession.toFixed(2)}/session`);
if (prediction.confidence >= 80) {
insights.push(`✅ High confidence prediction (${prediction.confidence}%)`);
}
else if (prediction.confidence >= 60) {
insights.push(`⚠️ Moderate confidence prediction (${prediction.confidence}%)`);
}
else {
insights.push(`❓ Low confidence prediction (${prediction.confidence}%) - need more data`);
}
if (prediction.pattern.averageTokensPerMessage.output > prediction.pattern.averageTokensPerMessage.input * 2) {
insights.push('💡 Consider using more concise prompts to reduce output tokens');
}
return insights;
}
formatPrediction(prediction) {
const lines = [
'🔮 Cost Prediction Report',
'='.repeat(40),
'',
'📊 Predicted Costs:',
` Tomorrow: $${prediction.nextDay.toFixed(2)}`,
` Next Week: $${prediction.nextWeek.toFixed(2)}`,
` Next Month: $${prediction.nextMonth.toFixed(2)}`,
'',
`📈 Based on ${prediction.basedOnDays} days of data`,
`🎯 Confidence: ${prediction.confidence}%`,
'',
'💡 Insights:',
...this.generateInsights(prediction).map(insight => ` ${insight}`)
];
return lines.join('\n');
}
}
//# sourceMappingURL=cost-predictor.js.map