UNPKG

cost-claude

Version:

Claude Code cost monitoring, analytics, and optimization toolkit

394 lines â€ĸ 17.2 kB
import { CostCalculator } from '../core/cost-calculator.js'; import chalk from 'chalk'; export class UsageInsightsAnalyzer { calculator; constructor() { this.calculator = new CostCalculator(); } async analyzeUsage(messages) { const insights = []; insights.push(...this.analyzeTokenUsage(messages)); insights.push(...this.analyzeCacheUsage(messages)); insights.push(...this.analyzeSessionPatterns(messages)); insights.push(...this.analyzeTimePatterns(messages)); insights.push(...this.analyzeCostAnomalies(messages)); insights.push(...this.analyzeModelUsage(messages)); const severityOrder = { critical: 0, warning: 1, info: 2, success: 3 }; insights.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); return insights; } analyzeTokenUsage(messages) { const insights = []; let totalInput = 0; let totalOutput = 0; let messageCount = 0; messages.forEach(msg => { if (msg.message && typeof msg.message === 'object' && msg.message.usage) { totalInput += msg.message.usage.input_tokens || 0; totalOutput += msg.message.usage.output_tokens || 0; messageCount++; } }); if (messageCount > 0) { const avgInput = totalInput / messageCount; const avgOutput = totalOutput / messageCount; const outputRatio = avgOutput / avgInput; if (outputRatio > 3) { insights.push({ type: 'optimization', severity: 'warning', title: 'High output-to-input ratio', description: `Your responses are ${outputRatio.toFixed(1)}x longer than your prompts on average`, impact: totalOutput * 0.2 * this.calculator.getRate('output') / 1000000, recommendation: 'Consider asking for more concise responses or using system prompts to limit output length' }); } if (avgInput > 5000) { insights.push({ type: 'optimization', severity: 'warning', title: 'Large average input size', description: `Average input is ${avgInput.toFixed(0)} tokens per message`, recommendation: 'Consider summarizing context or using conversation memory more efficiently' }); } const shortSessions = this.findShortSessions(messages); if (shortSessions.length > messages.length * 0.3) { insights.push({ type: 'pattern', severity: 'info', title: 'Many short conversations', description: `${shortSessions.length} sessions with fewer than 3 messages`, recommendation: 'Consider combining related queries into single sessions for better context' }); } } return insights; } analyzeCacheUsage(messages) { const insights = []; let totalCacheHits = 0; let totalCacheCreation = 0; let totalMessages = 0; messages.forEach(msg => { if (msg.message && typeof msg.message === 'object' && msg.message.usage) { const usage = msg.message.usage; if (usage.cache_read_input_tokens > 0) totalCacheHits++; if (usage.cache_creation_input_tokens > 0) totalCacheCreation++; totalMessages++; } }); if (totalMessages > 10) { const cacheHitRate = (totalCacheHits / totalMessages) * 100; const cacheCreationRate = (totalCacheCreation / totalMessages) * 100; if (cacheHitRate < 20 && totalMessages > 50) { insights.push({ type: 'optimization', severity: 'warning', title: 'Low cache utilization', description: `Only ${cacheHitRate.toFixed(1)}% of messages use cached context`, impact: totalMessages * 0.001, recommendation: 'Reuse conversations when working on the same project to benefit from context caching' }); } if (cacheHitRate > 70) { insights.push({ type: 'achievement', severity: 'success', title: 'Excellent cache usage!', description: `${cacheHitRate.toFixed(1)}% cache hit rate is saving you money`, impact: totalCacheHits * 0.002 }); } if (cacheCreationRate > 50) { insights.push({ type: 'pattern', severity: 'info', title: 'Frequent cache creation', description: 'You\'re creating new cached contexts frequently', recommendation: 'This is normal for diverse projects, but try to reuse contexts when possible' }); } } return insights; } analyzeSessionPatterns(messages) { const insights = []; const sessions = new Map(); messages.forEach(msg => { if (msg.sessionId) { if (!sessions.has(msg.sessionId)) { sessions.set(msg.sessionId, []); } sessions.get(msg.sessionId).push(msg); } }); const efficiencies = []; sessions.forEach((sessionMessages) => { const efficiency = this.calculateSessionEfficiency(sessionMessages); efficiencies.push(efficiency); }); const inefficientSessions = efficiencies.filter(e => e.efficiency < 50); if (inefficientSessions.length > 0) { const avgInefficiency = inefficientSessions.reduce((sum, e) => sum + e.efficiency, 0) / inefficientSessions.length; const totalWaste = inefficientSessions.reduce((sum, e) => sum + e.totalCost, 0) * 0.2; insights.push({ type: 'optimization', severity: 'warning', title: `${inefficientSessions.length} inefficient sessions detected`, description: `Average efficiency: ${avgInefficiency.toFixed(0)}%`, impact: totalWaste, recommendation: 'Review these sessions for repeated questions or unclear prompts' }); } const efficientSessions = efficiencies.filter(e => e.efficiency > 80); if (efficientSessions.length > 0) { insights.push({ type: 'achievement', severity: 'success', title: `${efficientSessions.length} highly efficient sessions!`, description: 'These sessions made good use of context and caching', recommendation: 'Study these patterns for future sessions' }); } return insights; } analyzeTimePatterns(messages) { const insights = []; const hourlyUsage = new Array(24).fill(0); const hourlyCost = new Array(24).fill(0); messages.forEach(msg => { if (msg.timestamp && msg.costUSD) { const hour = new Date(msg.timestamp).getHours(); hourlyUsage[hour]++; hourlyCost[hour] += msg.costUSD; } }); const peakHour = hourlyCost.indexOf(Math.max(...hourlyCost)); const peakCost = hourlyCost[peakHour]; const totalDailyCost = hourlyCost.reduce((sum, cost) => sum + cost, 0); if (peakCost > totalDailyCost * 0.3) { insights.push({ type: 'pattern', severity: 'info', title: `Peak usage at ${peakHour}:00-${peakHour + 1}:00`, description: `${((peakCost / totalDailyCost) * 100).toFixed(1)}% of daily cost occurs in this hour`, recommendation: 'Consider spreading work throughout the day for better cost tracking' }); } const lateNightUsage = hourlyUsage.slice(0, 6).reduce((sum, count) => sum + count, 0); const totalUsage = hourlyUsage.reduce((sum, count) => sum + count, 0); if (lateNightUsage > totalUsage * 0.3) { insights.push({ type: 'pattern', severity: 'info', title: 'Significant late-night usage', description: `${((lateNightUsage / totalUsage) * 100).toFixed(1)}% of usage occurs between midnight and 6 AM`, recommendation: 'Ensure you\'re getting enough rest! Consider time management strategies' }); } return insights; } analyzeCostAnomalies(messages) { const insights = []; const costs = messages .filter(msg => msg.costUSD && msg.costUSD > 0) .map(msg => msg.costUSD); if (costs.length < 10) return insights; const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length; const stdDev = Math.sqrt(costs.reduce((sum, cost) => sum + Math.pow(cost - mean, 2), 0) / costs.length); const outliers = messages.filter(msg => msg.costUSD && msg.costUSD > mean + 3 * stdDev); if (outliers.length > 0) { const outlierTotal = outliers.reduce((sum, msg) => sum + (msg.costUSD || 0), 0); insights.push({ type: 'anomaly', severity: 'warning', title: `${outliers.length} unusually expensive messages`, description: `Total cost: $${outlierTotal.toFixed(2)} (avg: $${(outlierTotal / outliers.length).toFixed(2)})`, recommendation: 'Review these messages for unnecessary token usage or repeated processing' }); } const dailyCosts = this.groupByDay(messages); const dailyValues = Array.from(dailyCosts.values()); if (dailyValues.length > 7) { const recentAvg = dailyValues.slice(-7).reduce((sum, cost) => sum + cost, 0) / 7; const previousAvg = dailyValues.slice(-14, -7).reduce((sum, cost) => sum + cost, 0) / 7; if (recentAvg > previousAvg * 2) { insights.push({ type: 'anomaly', severity: 'critical', title: 'Significant cost increase detected', description: `Recent weekly average is ${((recentAvg / previousAvg - 1) * 100).toFixed(0)}% higher`, impact: (recentAvg - previousAvg) * 7, recommendation: 'Review recent usage patterns and consider setting budget alerts' }); } } return insights; } analyzeModelUsage(messages) { const insights = []; const modelUsage = new Map(); const modelCost = new Map(); messages.forEach(msg => { if (msg.message && typeof msg.message === 'object' && msg.message.model) { const model = msg.message.model; modelUsage.set(model, (modelUsage.get(model) || 0) + 1); modelCost.set(model, (modelCost.get(model) || 0) + (msg.costUSD || 0)); } }); modelUsage.forEach((count, model) => { const avgCost = (modelCost.get(model) || 0) / count; if (model.includes('opus') && avgCost < 0.01) { insights.push({ type: 'optimization', severity: 'info', title: 'Consider using a lighter model', description: `You're using ${model} for tasks averaging $${avgCost.toFixed(3)}`, impact: count * avgCost * 0.5, recommendation: 'For simple tasks, consider using Haiku or Sonnet models' }); } }); return insights; } calculateSessionEfficiency(messages) { const sessionId = messages[0]?.sessionId || 'unknown'; const totalCost = messages.reduce((sum, msg) => sum + (msg.costUSD || 0), 0); const messageCount = messages.length; const avgCostPerMessage = totalCost / messageCount; let cacheHits = 0; let totalWithUsage = 0; messages.forEach(msg => { if (msg.message && typeof msg.message === 'object' && msg.message.usage) { totalWithUsage++; if (msg.message.usage.cache_read_input_tokens > 0) { cacheHits++; } } }); const cacheHitRate = totalWithUsage > 0 ? (cacheHits / totalWithUsage) * 100 : 0; const factors = []; let efficiency = 50; if (cacheHitRate > 50) { efficiency += 20; factors.push('Good cache usage'); } else if (cacheHitRate < 10) { efficiency -= 10; factors.push('Low cache usage'); } if (messageCount > 10) { efficiency += 10; factors.push('Good session length'); } else if (messageCount < 3) { efficiency -= 20; factors.push('Very short session'); } if (avgCostPerMessage < 0.05) { efficiency += 10; factors.push('Cost-effective messages'); } else if (avgCostPerMessage > 0.20) { efficiency -= 20; factors.push('Expensive messages'); } const messageTexts = messages .filter(msg => msg.message && typeof msg.message === 'object') .map(msg => msg.message.content); const uniqueMessages = new Set(messageTexts).size; const repetitionRate = 1 - (uniqueMessages / messageTexts.length); if (repetitionRate > 0.2) { efficiency -= 15; factors.push('Repeated questions detected'); } efficiency = Math.max(0, Math.min(100, efficiency)); return { sessionId, efficiency, totalCost, messageCount, avgCostPerMessage, cacheHitRate, factors }; } findShortSessions(messages) { const sessionCounts = new Map(); messages.forEach(msg => { if (msg.sessionId) { sessionCounts.set(msg.sessionId, (sessionCounts.get(msg.sessionId) || 0) + 1); } }); return Array.from(sessionCounts.entries()) .filter(([_, count]) => count < 3) .map(([sessionId, _]) => sessionId); } groupByDay(messages) { const dailyCosts = new Map(); messages.forEach(msg => { if (msg.timestamp && msg.costUSD) { const date = new Date(msg.timestamp).toDateString(); dailyCosts.set(date, (dailyCosts.get(date) || 0) + msg.costUSD); } }); return dailyCosts; } formatInsights(insights) { if (insights.length === 0) { return chalk.green('✨ No significant insights found. Your usage looks good!'); } const lines = [ chalk.bold('📊 Usage Insights Report'), '='.repeat(50), '' ]; const iconMap = { critical: '🚨', warning: 'âš ī¸', info: 'â„šī¸', success: '✅' }; const colorMap = { critical: chalk.red, warning: chalk.yellow, info: chalk.blue, success: chalk.green }; insights.forEach(insight => { const icon = iconMap[insight.severity]; const color = colorMap[insight.severity]; lines.push(color(`${icon} ${insight.title}`)); lines.push(` ${insight.description}`); if (insight.impact !== undefined) { lines.push(chalk.dim(` 💰 Potential savings: $${insight.impact.toFixed(2)}`)); } if (insight.recommendation) { lines.push(chalk.dim(` 💡 ${insight.recommendation}`)); } lines.push(''); }); const criticalCount = insights.filter(i => i.severity === 'critical').length; const warningCount = insights.filter(i => i.severity === 'warning').length; const totalSavings = insights .filter(i => i.impact !== undefined) .reduce((sum, i) => sum + (i.impact || 0), 0); lines.push('-'.repeat(50)); lines.push(chalk.bold('Summary:')); if (criticalCount > 0) { lines.push(chalk.red(` 🚨 ${criticalCount} critical issues`)); } if (warningCount > 0) { lines.push(chalk.yellow(` âš ī¸ ${warningCount} warnings`)); } if (totalSavings > 0) { lines.push(chalk.green(` 💰 Potential savings: $${totalSavings.toFixed(2)}`)); } return lines.join('\n'); } } //# sourceMappingURL=usage-insights.js.map