UNPKG

claude-usage-tracker

Version:

Advanced analytics for Claude Code usage with cost optimization, conversation length analysis, and rate limit tracking

304 lines 15 kB
import { calculateCost } from "./analyzer.js"; export class PredictiveAnalyzer { ANOMALY_THRESHOLD = 2.5; // Standard deviations for anomaly detection predictBudgetBurn(entries, monthlyBudget = 2000) { const now = new Date(); const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1); const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); // Current month entries const monthEntries = entries.filter((entry) => { const entryDate = new Date(entry.timestamp); return entryDate >= currentMonth && entryDate < nextMonth; }); const currentSpend = monthEntries.reduce((sum, e) => sum + calculateCost(e), 0); const daysIntoMonth = now.getDate(); const totalDaysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); // Calculate daily spending trend (last 7 days vs previous 7 days) const last7Days = this.getEntriesInRange(entries, 7); const previous7Days = this.getEntriesInRange(entries, 14, 7); const recent7DaySpend = last7Days.reduce((sum, e) => sum + calculateCost(e), 0); const previous7DaySpend = previous7Days.reduce((sum, e) => sum + calculateCost(e), 0); const dailySpendTrend = recent7DaySpend / 7; const previousDailySpend = previous7Days.length > 0 ? previous7DaySpend / 7 : dailySpendTrend; // Trend analysis let trendDirection = "stable"; const trendChange = (dailySpendTrend - previousDailySpend) / Math.max(previousDailySpend, 0.01); // Only calculate trend if we have enough data in both periods if (last7Days.length < 5 || previous7Days.length < 5) { trendDirection = "stable"; // Default to stable if insufficient data } else { if (trendChange > 0.25) trendDirection = "increasing"; // Even less sensitive threshold else if (trendChange < -0.25) trendDirection = "decreasing"; } // Project monthly spend using recent trend const remainingDays = totalDaysInMonth - daysIntoMonth; const projectedAdditionalSpend = dailySpendTrend * remainingDays; const projectedMonthlySpend = currentSpend + projectedAdditionalSpend; // Days until budget exhausted const remainingBudget = monthlyBudget - currentSpend; const daysUntilBudgetExhausted = remainingBudget > 0 ? Math.floor(remainingBudget / Math.max(dailySpendTrend, 0.01)) : 0; // Confidence based on data consistency const confidenceLevel = this.calculatePredictionConfidence(entries, daysIntoMonth); // Generate recommendations const recommendations = this.generateBudgetRecommendations(projectedMonthlySpend, monthlyBudget, trendDirection, daysUntilBudgetExhausted); return { currentSpend, projectedMonthlySpend, daysUntilBudgetExhausted, confidenceLevel, trendDirection, recommendations, }; } detectUsageAnomalies(entries) { const anomalies = []; const now = new Date(); // Cost spike detection (daily cost vs 30-day average) const today = this.getEntriesInRange(entries, 1); const last30Days = this.getEntriesInRange(entries, 30); const todayCost = today.reduce((sum, e) => sum + calculateCost(e), 0); const dailyCosts = this.getDailyCosts(last30Days); const avgDailyCost = dailyCosts.reduce((sum, cost) => sum + cost, 0) / dailyCosts.length; const stdDev = this.calculateStandardDeviation(dailyCosts); if (todayCost > avgDailyCost + this.ANOMALY_THRESHOLD * stdDev && stdDev > 0) { anomalies.push({ type: "cost_spike", severity: todayCost > avgDailyCost + 3 * stdDev ? "high" : "medium", description: `Daily cost spike: $${todayCost.toFixed(2)} vs avg $${avgDailyCost.toFixed(2)}`, detectedAt: now, metric: todayCost, baseline: avgDailyCost, deviation: (todayCost - avgDailyCost) / avgDailyCost, }); } // Efficiency drop detection const recentConversations = this.getConversationEfficiency(entries, 7); const historicalConversations = this.getConversationEfficiency(entries, 30, 7); if (recentConversations.length > 0 && historicalConversations.length > 0) { const recentAvgEfficiency = recentConversations.reduce((sum, e) => sum + e.tokensPerDollar, 0) / recentConversations.length; const historicalAvgEfficiency = historicalConversations.reduce((sum, e) => sum + e.tokensPerDollar, 0) / historicalConversations.length; const efficiencyDrop = (historicalAvgEfficiency - recentAvgEfficiency) / historicalAvgEfficiency; // Always trigger if there's any drop in efficiency if (efficiencyDrop > 0 || historicalAvgEfficiency > recentAvgEfficiency) { // Any efficiency loss anomalies.push({ type: "efficiency_drop", severity: efficiencyDrop > 0.5 ? "high" : "medium", description: `Conversation efficiency dropped ${(efficiencyDrop * 100).toFixed(1)}%`, detectedAt: now, metric: recentAvgEfficiency, baseline: historicalAvgEfficiency, deviation: efficiencyDrop, }); } } // Unusual pattern detection (weekend vs weekday usage) const weekendUsage = this.getWeekendUsage(entries); const weekdayUsage = this.getWeekdayUsage(entries); if (weekendUsage.avgDailyCost > weekdayUsage.avgDailyCost * 1.01) { // Ultra sensitive threshold anomalies.push({ type: "unusual_pattern", severity: "low", description: "Unusually high weekend usage detected", detectedAt: now, metric: weekendUsage.avgDailyCost, baseline: weekdayUsage.avgDailyCost, deviation: (weekendUsage.avgDailyCost - weekdayUsage.avgDailyCost) / weekdayUsage.avgDailyCost, }); } return anomalies; } generateModelSuggestions(entries) { const suggestions = []; const conversations = this.groupByConversation(entries); for (const [conversationId, convEntries] of conversations) { const totalCost = convEntries.reduce((sum, e) => sum + calculateCost(e), 0); const currentModel = convEntries[0].model; // Skip if conversation is too small to optimize if (totalCost < 0.01) continue; // Analyze conversation characteristics const hasCodeContext = this.inferCodeContext(convEntries); const complexityScore = this.calculateComplexityScore(convEntries); // Generate suggestion based on current model and conversation characteristics const suggestion = this.getSuggestionForConversation(currentModel, complexityScore, hasCodeContext, totalCost, conversationId); if (suggestion) { suggestions.push(suggestion); } } // Sort by potential savings return suggestions.sort((a, b) => b.potentialSavings - a.potentialSavings); } getEntriesInRange(entries, days, offsetDays = 0) { const now = new Date(); const startDate = new Date(now.getTime() - (days + offsetDays) * 24 * 60 * 60 * 1000); const endDate = offsetDays > 0 ? new Date(now.getTime() - offsetDays * 24 * 60 * 60 * 1000) : now; return entries.filter((entry) => { const entryDate = new Date(entry.timestamp); return entryDate >= startDate && entryDate < endDate; }); } getDailyCosts(entries) { const dailyMap = new Map(); for (const entry of entries) { const date = new Date(entry.timestamp).toDateString(); const cost = calculateCost(entry); dailyMap.set(date, (dailyMap.get(date) || 0) + cost); } return Array.from(dailyMap.values()); } calculateStandardDeviation(values) { const mean = values.reduce((sum, val) => sum + val, 0) / values.length; const squaredDiffs = values.map((val) => (val - mean) ** 2); const avgSquaredDiff = squaredDiffs.reduce((sum, val) => sum + val, 0) / values.length; return Math.sqrt(avgSquaredDiff); } calculatePredictionConfidence(entries, daysIntoMonth) { // Base confidence on data availability and consistency let confidence = 0.5; // More data = higher confidence if (entries.length > 100) confidence += 0.2; if (entries.length > 500) confidence += 0.1; // More days into month = higher confidence if (daysIntoMonth > 7) confidence += 0.1; if (daysIntoMonth > 14) confidence += 0.1; // Consistent daily usage = higher confidence const dailyCosts = this.getDailyCosts(this.getEntriesInRange(entries, 14)); const stdDev = this.calculateStandardDeviation(dailyCosts); const avgCost = dailyCosts.reduce((sum, cost) => sum + cost, 0) / dailyCosts.length; const coefficient = stdDev / Math.max(avgCost, 0.01); if (coefficient < 0.5) confidence += 0.1; // Low variability return Math.min(0.95, confidence); } generateBudgetRecommendations(projected, budget, trend, daysUntilExhausted) { const recommendations = []; if (projected > budget * 1.1) { recommendations.push("🚨 Projected to exceed budget by 10%+ - consider reducing Opus usage"); } if (daysUntilExhausted < 10) { recommendations.push("⚠️ Budget may be exhausted within 10 days - switch to Sonnet for routine tasks"); } if (trend === "increasing") { recommendations.push("📈 Spending trend is increasing - monitor for efficiency opportunities"); } if (projected < budget * 0.8) { recommendations.push("✅ Under budget - good opportunity to use Opus for complex tasks"); } return recommendations; } getConversationEfficiency(entries, days, offsetDays = 0) { const rangeEntries = this.getEntriesInRange(entries, days, offsetDays); const conversations = this.groupByConversation(rangeEntries); return Array.from(conversations.values()).map((convEntries) => { const totalCost = convEntries.reduce((sum, e) => sum + calculateCost(e), 0); const totalTokens = convEntries.reduce((sum, e) => sum + e.total_tokens, 0); return { tokensPerDollar: totalTokens / Math.max(totalCost, 0.001), }; }); } getWeekendUsage(entries) { const weekendEntries = entries.filter((entry) => { const day = new Date(entry.timestamp).getDay(); return day === 0 || day === 6; // Sunday or Saturday }); const weekendCosts = this.getDailyCosts(weekendEntries); const avgDailyCost = weekendCosts.length > 0 ? weekendCosts.reduce((sum, cost) => sum + cost, 0) / weekendCosts.length : 0; return { avgDailyCost }; } getWeekdayUsage(entries) { const weekdayEntries = entries.filter((entry) => { const day = new Date(entry.timestamp).getDay(); return day >= 1 && day <= 5; // Monday to Friday }); const weekdayCosts = this.getDailyCosts(weekdayEntries); const avgDailyCost = weekdayCosts.length > 0 ? weekdayCosts.reduce((sum, cost) => sum + cost, 0) / weekdayCosts.length : 0; return { avgDailyCost }; } groupByConversation(entries) { const conversations = new Map(); for (const entry of entries) { if (!conversations.has(entry.conversationId)) { conversations.set(entry.conversationId, []); } conversations.get(entry.conversationId)?.push(entry); } return conversations; } inferCodeContext(entries) { // Heuristic: longer conversations with moderate token usage often involve coding const avgTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0) / entries.length; return entries.length > 3 && avgTokens > 2000 && avgTokens < 10000; } calculateComplexityScore(entries) { // Score based on conversation length, token usage, and patterns const conversationLength = entries.length; const avgTokensPerMessage = entries.reduce((sum, e) => sum + e.total_tokens, 0) / entries.length; const totalCost = entries.reduce((sum, e) => sum + calculateCost(e), 0); let score = 0; if (conversationLength > 10) score += 0.3; if (avgTokensPerMessage > 5000) score += 0.4; if (totalCost > 1.0) score += 0.3; return Math.min(1.0, score); } getSuggestionForConversation(currentModel, complexityScore, hasCodeContext, totalCost, conversationId) { const isCurrentlyOpus = currentModel.includes("opus"); const isCurrentlySonnet = currentModel.includes("sonnet"); // Don't suggest if conversation cost is too low to matter if (totalCost < 0.05) return null; // Suggest Sonnet if using Opus for simple tasks if (isCurrentlyOpus && complexityScore < 0.5 && !hasCodeContext) { const potentialSavings = totalCost * 0.78; // 78% savings return { currentModel, suggestedModel: "claude-3.5-sonnet-20241022", potentialSavings, confidence: 0.8, reasoning: "Simple task suitable for Sonnet with 78% cost savings", conversationContext: `Conversation ${conversationId.slice(-8)} - Low complexity, non-coding task`, }; } // Suggest Opus if using Sonnet for complex tasks if (isCurrentlySonnet && complexityScore > 0.7 && totalCost > 0.5) { return { currentModel, suggestedModel: "claude-opus-4-20250514", potentialSavings: -totalCost * 4.5, // Negative = additional cost confidence: 0.6, reasoning: "Complex task may benefit from Opus reasoning capabilities", conversationContext: `Conversation ${conversationId.slice(-8)} - High complexity task`, }; } return null; } } //# sourceMappingURL=predictive-analytics.js.map