cost-claude
Version:
Claude Code cost monitoring, analytics, and optimization toolkit
247 lines • 10.3 kB
JavaScript
import { ProjectParser } from '../core/project-parser.js';
import { formatDate } from '../utils/format.js';
export class GroupAnalyzer {
parser;
calculator;
constructor(parser, calculator) {
this.parser = parser;
this.calculator = calculator;
}
groupByProject(messages) {
const grouped = ProjectParser.groupByProject(messages);
const results = [];
grouped.forEach((projectMessages, projectName) => {
const stats = this.calculateGroupStats(projectMessages, projectName);
if (stats.messageCount > 0) {
results.push(stats);
}
});
return results.sort((a, b) => b.totalCost - a.totalCost);
}
groupByDate(messages) {
const grouped = new Map();
messages.forEach(msg => {
if (!msg.timestamp)
return;
const timestamp = new Date(msg.timestamp);
if (isNaN(timestamp.getTime()))
return;
const date = formatDate(timestamp);
if (date === 'Invalid Date')
return;
if (!grouped.has(date)) {
grouped.set(date, []);
}
grouped.get(date).push(msg);
});
const results = [];
grouped.forEach((dateMessages, date) => {
const stats = this.calculateGroupStats(dateMessages, date);
if (stats.messageCount > 0) {
results.push(stats);
}
});
return results.sort((a, b) => a.groupName.localeCompare(b.groupName));
}
groupByHour(messages) {
const grouped = new Map();
messages.forEach(msg => {
if (!msg.timestamp)
return;
const timestamp = new Date(msg.timestamp);
if (isNaN(timestamp.getTime()))
return;
const date = formatDate(timestamp);
if (date === 'Invalid Date')
return;
const hourKey = `${date} ${timestamp.getHours().toString().padStart(2, '0')}:00`;
if (!grouped.has(hourKey)) {
grouped.set(hourKey, []);
}
grouped.get(hourKey).push(msg);
});
const results = [];
grouped.forEach((hourMessages, hour) => {
const stats = this.calculateGroupStats(hourMessages, hour);
if (stats.messageCount > 0) {
results.push(stats);
}
});
return results.sort((a, b) => a.groupName.localeCompare(b.groupName));
}
groupBySession(messages, filterLastWeek = false) {
const grouped = this.parser.groupBySession(messages);
const results = [];
grouped.forEach((sessionMessages, sessionId) => {
const stats = this.calculateGroupStats(sessionMessages, sessionId);
if (stats.messageCount > 0) {
if (sessionMessages.length > 0) {
const firstMessage = sessionMessages[0];
if (firstMessage) {
stats.projectName = ProjectParser.getProjectFromMessage(firstMessage);
}
if (stats.startTime && stats.endTime) {
const start = new Date(stats.startTime);
const end = new Date(stats.endTime);
if (formatDate(start) === formatDate(end)) {
stats.dateRange = formatDate(start);
}
else {
const startMonth = start.getMonth() + 1;
const startDay = start.getDate();
const endMonth = end.getMonth() + 1;
const endDay = end.getDate();
if (start.getFullYear() === end.getFullYear() && startMonth === endMonth) {
stats.dateRange = `${start.getFullYear()}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}~${String(endDay).padStart(2, '0')}`;
}
else {
stats.dateRange = `${String(startMonth).padStart(2, '0')}/${String(startDay).padStart(2, '0')}~${String(endMonth).padStart(2, '0')}/${String(endDay).padStart(2, '0')}`;
}
}
}
}
if (filterLastWeek && stats.endTime) {
const endDate = new Date(stats.endTime);
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
oneWeekAgo.setHours(0, 0, 0, 0);
if (endDate < oneWeekAgo) {
return;
}
}
results.push(stats);
}
});
return results.sort((a, b) => b.totalCost - a.totalCost);
}
calculateGroupStats(messages, groupName) {
const assistantMessages = this.parser.filterByType(messages, 'assistant');
let totalCost = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let totalCacheTokens = 0;
let totalDuration = 0;
let totalCacheCreationTokens = 0;
let inputCost = 0;
let outputCost = 0;
let cacheWriteCost = 0;
let cacheReadCost = 0;
assistantMessages.forEach(msg => {
if (msg.costUSD !== null && msg.costUSD !== undefined) {
totalCost += msg.costUSD;
}
else {
const content = this.parser.parseMessageContent(msg);
if (content?.usage) {
totalCost += this.calculator.calculate(content.usage);
}
}
if (msg.durationMs) {
totalDuration += msg.durationMs;
}
const content = this.parser.parseMessageContent(msg);
if (content?.usage) {
totalInputTokens += content.usage.input_tokens || 0;
totalOutputTokens += content.usage.output_tokens || 0;
totalCacheTokens += content.usage.cache_read_input_tokens || 0;
totalCacheCreationTokens += content.usage.cache_creation_input_tokens || 0;
const breakdown = this.calculator.calculateBreakdown(content.usage);
inputCost += breakdown.inputTokensCost;
outputCost += breakdown.outputTokensCost;
cacheWriteCost += breakdown.cacheCreationCost;
cacheReadCost += breakdown.cacheReadCost;
}
});
const cacheEfficiency = this.calculator.calculateCacheEfficiency({
input_tokens: totalInputTokens,
cache_read_input_tokens: totalCacheTokens,
cache_creation_input_tokens: totalCacheCreationTokens,
});
const sorted = this.parser.sortByTimestamp(messages);
const startTime = sorted.length > 0 ? sorted[0]?.timestamp : undefined;
const endTime = sorted.length > 0 ? sorted[sorted.length - 1]?.timestamp : undefined;
const duration = this.parser.calculateSessionDuration(messages);
return {
groupName,
totalCost,
messageCount: assistantMessages.length,
avgCost: assistantMessages.length > 0 ? totalCost / assistantMessages.length : 0,
duration,
tokens: {
input: totalInputTokens,
output: totalOutputTokens,
cache: totalCacheTokens,
cacheWrite: totalCacheCreationTokens,
},
cacheEfficiency,
costBreakdown: {
inputCost,
outputCost,
cacheWriteCost,
cacheReadCost,
},
startTime,
endTime,
};
}
groupByModel(messages) {
const grouped = new Map();
messages.forEach(msg => {
if (msg.type === 'assistant') {
const content = this.parser.parseMessageContent(msg);
const model = content?.model || 'unknown';
if (!grouped.has(model)) {
grouped.set(model, []);
}
grouped.get(model).push(msg);
}
});
const results = [];
grouped.forEach((modelMessages, model) => {
const stats = this.calculateGroupStats(modelMessages, model);
if (stats.messageCount > 0) {
results.push(stats);
}
});
return results.sort((a, b) => b.totalCost - a.totalCost);
}
groupByCostRange(messages) {
const ranges = [
{ name: 'High (>$1)', min: 1, max: Infinity },
{ name: 'Medium ($0.1-$1)', min: 0.1, max: 1 },
{ name: 'Low ($0.01-$0.1)', min: 0.01, max: 0.1 },
{ name: 'Very Low (<$0.01)', min: 0, max: 0.01 },
];
const grouped = new Map();
ranges.forEach(range => grouped.set(range.name, []));
messages.forEach(msg => {
if (msg.type === 'assistant') {
let msgCost = 0;
if (msg.costUSD !== null && msg.costUSD !== undefined) {
msgCost = msg.costUSD;
}
else {
const content = this.parser.parseMessageContent(msg);
if (content?.usage) {
msgCost = this.calculator.calculate(content.usage);
}
}
if (msgCost > 0) {
const range = ranges.find(r => msgCost >= r.min && msgCost < r.max);
if (range) {
grouped.get(range.name).push(msg);
}
}
}
});
const results = [];
grouped.forEach((rangeMessages, rangeName) => {
const stats = this.calculateGroupStats(rangeMessages, rangeName);
if (stats.messageCount > 0) {
results.push(stats);
}
});
return results;
}
}
//# sourceMappingURL=group-analyzer.js.map