@tokscale/cli 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +14 -3
- package/dist/cli.js.map +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +3 -2
- package/dist/native.js.map +1 -1
- package/package.json +6 -4
- package/src/auth.ts +211 -0
- package/src/cli.ts +1040 -0
- package/src/credentials.ts +123 -0
- package/src/cursor.ts +558 -0
- package/src/graph-types.ts +188 -0
- package/src/graph.ts +485 -0
- package/src/native-runner.ts +105 -0
- package/src/native.ts +938 -0
- package/src/pricing.ts +309 -0
- package/src/sessions/claudecode.ts +119 -0
- package/src/sessions/codex.ts +227 -0
- package/src/sessions/gemini.ts +108 -0
- package/src/sessions/index.ts +126 -0
- package/src/sessions/opencode.ts +94 -0
- package/src/sessions/reports.ts +475 -0
- package/src/sessions/types.ts +59 -0
- package/src/spinner.ts +283 -0
- package/src/submit.ts +175 -0
- package/src/table.ts +233 -0
- package/src/tui/App.tsx +339 -0
- package/src/tui/components/BarChart.tsx +198 -0
- package/src/tui/components/DailyView.tsx +113 -0
- package/src/tui/components/DateBreakdownPanel.tsx +79 -0
- package/src/tui/components/Footer.tsx +225 -0
- package/src/tui/components/Header.tsx +68 -0
- package/src/tui/components/Legend.tsx +39 -0
- package/src/tui/components/LoadingSpinner.tsx +82 -0
- package/src/tui/components/ModelRow.tsx +47 -0
- package/src/tui/components/ModelView.tsx +145 -0
- package/src/tui/components/OverviewView.tsx +108 -0
- package/src/tui/components/StatsView.tsx +225 -0
- package/src/tui/components/TokenBreakdown.tsx +46 -0
- package/src/tui/components/index.ts +15 -0
- package/src/tui/config/settings.ts +130 -0
- package/src/tui/config/themes.ts +115 -0
- package/src/tui/hooks/useData.ts +518 -0
- package/src/tui/index.tsx +44 -0
- package/src/tui/opentui.d.ts +137 -0
- package/src/tui/types/index.ts +165 -0
- package/src/tui/utils/cleanup.ts +65 -0
- package/src/tui/utils/colors.ts +65 -0
- package/src/tui/utils/format.ts +36 -0
- package/src/tui/utils/responsive.ts +8 -0
- package/src/types.d.ts +28 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for contribution graph data
|
|
3
|
+
* Note: intensity is calculated based on COST ($), not tokens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid source identifiers
|
|
8
|
+
*/
|
|
9
|
+
export type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Token breakdown by category
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenBreakdown {
|
|
15
|
+
input: number;
|
|
16
|
+
output: number;
|
|
17
|
+
cacheRead: number;
|
|
18
|
+
cacheWrite: number;
|
|
19
|
+
reasoning: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Per-source contribution for a single day
|
|
24
|
+
*/
|
|
25
|
+
export interface SourceContribution {
|
|
26
|
+
/** Source identifier */
|
|
27
|
+
source: SourceType;
|
|
28
|
+
|
|
29
|
+
/** Exact model ID as reported by the source */
|
|
30
|
+
modelId: string;
|
|
31
|
+
|
|
32
|
+
/** Provider ID if available */
|
|
33
|
+
providerId?: string;
|
|
34
|
+
|
|
35
|
+
/** Token counts */
|
|
36
|
+
tokens: TokenBreakdown;
|
|
37
|
+
|
|
38
|
+
/** Calculated cost for this source/model combination */
|
|
39
|
+
cost: number;
|
|
40
|
+
|
|
41
|
+
/** Number of messages/requests */
|
|
42
|
+
messages: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Daily contribution entry with full granularity
|
|
47
|
+
*/
|
|
48
|
+
export interface DailyContribution {
|
|
49
|
+
/** ISO date string (YYYY-MM-DD) */
|
|
50
|
+
date: string;
|
|
51
|
+
|
|
52
|
+
/** Aggregated totals for the day */
|
|
53
|
+
totals: {
|
|
54
|
+
/** Total tokens (input + output + cache) */
|
|
55
|
+
tokens: number;
|
|
56
|
+
/** Total cost in USD */
|
|
57
|
+
cost: number;
|
|
58
|
+
/** Number of messages/requests */
|
|
59
|
+
messages: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculated intensity grade (0-4)
|
|
64
|
+
* Based on COST, not tokens
|
|
65
|
+
* 0 = no activity, 4 = highest cost relative to max
|
|
66
|
+
*/
|
|
67
|
+
intensity: 0 | 1 | 2 | 3 | 4;
|
|
68
|
+
|
|
69
|
+
/** Token breakdown by category (aggregated across all sources) */
|
|
70
|
+
tokenBreakdown: TokenBreakdown;
|
|
71
|
+
|
|
72
|
+
/** Per-source breakdown with model information */
|
|
73
|
+
sources: SourceContribution[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Year-level summary
|
|
78
|
+
*/
|
|
79
|
+
export interface YearSummary {
|
|
80
|
+
/** Year as string (e.g., "2024") */
|
|
81
|
+
year: string;
|
|
82
|
+
|
|
83
|
+
/** Total tokens for the year */
|
|
84
|
+
totalTokens: number;
|
|
85
|
+
|
|
86
|
+
/** Total cost for the year */
|
|
87
|
+
totalCost: number;
|
|
88
|
+
|
|
89
|
+
/** Date range for this year's data */
|
|
90
|
+
range: {
|
|
91
|
+
start: string;
|
|
92
|
+
end: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Summary statistics
|
|
98
|
+
*/
|
|
99
|
+
export interface DataSummary {
|
|
100
|
+
/** Total tokens across all time */
|
|
101
|
+
totalTokens: number;
|
|
102
|
+
|
|
103
|
+
/** Total cost across all time */
|
|
104
|
+
totalCost: number;
|
|
105
|
+
|
|
106
|
+
/** Total number of days in date range */
|
|
107
|
+
totalDays: number;
|
|
108
|
+
|
|
109
|
+
/** Number of days with activity */
|
|
110
|
+
activeDays: number;
|
|
111
|
+
|
|
112
|
+
/** Average cost per day (based on active days) */
|
|
113
|
+
averagePerDay: number;
|
|
114
|
+
|
|
115
|
+
/** Maximum cost in a single day (used for intensity calculation) */
|
|
116
|
+
maxCostInSingleDay: number;
|
|
117
|
+
|
|
118
|
+
/** All sources present in the data */
|
|
119
|
+
sources: SourceType[];
|
|
120
|
+
|
|
121
|
+
/** All unique model IDs across all sources */
|
|
122
|
+
models: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Metadata about the export
|
|
127
|
+
*/
|
|
128
|
+
export interface ExportMeta {
|
|
129
|
+
/** ISO timestamp of when the data was generated */
|
|
130
|
+
generatedAt: string;
|
|
131
|
+
|
|
132
|
+
/** CLI version that generated this data */
|
|
133
|
+
version: string;
|
|
134
|
+
|
|
135
|
+
/** Date range of the data */
|
|
136
|
+
dateRange: {
|
|
137
|
+
start: string;
|
|
138
|
+
end: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Root data structure exported by CLI
|
|
144
|
+
* This is the complete JSON schema for contribution graph data
|
|
145
|
+
*/
|
|
146
|
+
export interface TokenContributionData {
|
|
147
|
+
/** Metadata about the export */
|
|
148
|
+
meta: ExportMeta;
|
|
149
|
+
|
|
150
|
+
/** Summary statistics */
|
|
151
|
+
summary: DataSummary;
|
|
152
|
+
|
|
153
|
+
/** Year-by-year breakdown for multi-year views */
|
|
154
|
+
years: YearSummary[];
|
|
155
|
+
|
|
156
|
+
/** Daily contribution data - the core dataset */
|
|
157
|
+
contributions: DailyContribution[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Options for graph data generation
|
|
162
|
+
*/
|
|
163
|
+
export interface GraphOptions {
|
|
164
|
+
/** Filter to specific sources */
|
|
165
|
+
sources?: SourceType[];
|
|
166
|
+
|
|
167
|
+
/** Start date filter (ISO format) */
|
|
168
|
+
since?: string;
|
|
169
|
+
|
|
170
|
+
/** End date filter (ISO format) */
|
|
171
|
+
until?: string;
|
|
172
|
+
|
|
173
|
+
/** Filter to specific year */
|
|
174
|
+
year?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Unified message format for aggregation
|
|
179
|
+
* Used internally to normalize data from different sources
|
|
180
|
+
*/
|
|
181
|
+
export interface UnifiedMessage {
|
|
182
|
+
source: SourceType;
|
|
183
|
+
modelId: string;
|
|
184
|
+
providerId?: string;
|
|
185
|
+
timestamp: number; // Unix milliseconds
|
|
186
|
+
tokens: TokenBreakdown;
|
|
187
|
+
cost: number;
|
|
188
|
+
}
|
package/src/graph.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph data generation module
|
|
3
|
+
* Aggregates token usage data by date for contribution graph visualization
|
|
4
|
+
*
|
|
5
|
+
* Key design: intensity is calculated based on COST ($), not tokens
|
|
6
|
+
*
|
|
7
|
+
* This module supports two implementations:
|
|
8
|
+
* - Native Rust (fast, ~10x faster) - used when available
|
|
9
|
+
* - Pure TypeScript (fallback) - always available
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { format } from "date-fns";
|
|
13
|
+
import {
|
|
14
|
+
parseOpenCodeMessages,
|
|
15
|
+
parseClaudeCodeMessages,
|
|
16
|
+
parseCodexMessages,
|
|
17
|
+
parseGeminiMessages,
|
|
18
|
+
} from "./sessions/index.js";
|
|
19
|
+
import { PricingFetcher } from "./pricing.js";
|
|
20
|
+
import type {
|
|
21
|
+
TokenContributionData,
|
|
22
|
+
DailyContribution,
|
|
23
|
+
YearSummary,
|
|
24
|
+
DataSummary,
|
|
25
|
+
GraphOptions,
|
|
26
|
+
UnifiedMessage,
|
|
27
|
+
SourceType,
|
|
28
|
+
} from "./graph-types.js";
|
|
29
|
+
|
|
30
|
+
const VERSION = "1.0.0";
|
|
31
|
+
|
|
32
|
+
// Try to load native module
|
|
33
|
+
let nativeModule: typeof import("./native.js") | null = null;
|
|
34
|
+
try {
|
|
35
|
+
nativeModule = await import("./native.js");
|
|
36
|
+
} catch {
|
|
37
|
+
// Native module not available, will use TypeScript implementation
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if native implementation is available
|
|
42
|
+
*/
|
|
43
|
+
export function isNativeAvailable(): boolean {
|
|
44
|
+
return nativeModule?.isNativeAvailable() ?? false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate contribution graph data from all sources
|
|
49
|
+
*
|
|
50
|
+
* Uses native Rust implementation if available, falls back to TypeScript.
|
|
51
|
+
* Set `options.forceTypescript = true` to skip native module.
|
|
52
|
+
*/
|
|
53
|
+
export async function generateGraphData(
|
|
54
|
+
options: GraphOptions & { forceTypescript?: boolean } = {}
|
|
55
|
+
): Promise<TokenContributionData> {
|
|
56
|
+
// Try native implementation first (unless forced to use TypeScript)
|
|
57
|
+
if (!options.forceTypescript && nativeModule?.isNativeAvailable()) {
|
|
58
|
+
try {
|
|
59
|
+
return nativeModule.generateGraphNative(options);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Fall through to TypeScript implementation
|
|
62
|
+
console.warn("Native module failed, falling back to TypeScript:", e);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// TypeScript implementation
|
|
67
|
+
return generateGraphDataTS(options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pure TypeScript implementation of graph data generation
|
|
72
|
+
*/
|
|
73
|
+
export async function generateGraphDataTS(
|
|
74
|
+
options: GraphOptions = {}
|
|
75
|
+
): Promise<TokenContributionData> {
|
|
76
|
+
const fetcher = new PricingFetcher();
|
|
77
|
+
await fetcher.fetchPricing();
|
|
78
|
+
|
|
79
|
+
// Collect all messages from enabled sources
|
|
80
|
+
const messages = collectMessages(options, fetcher);
|
|
81
|
+
|
|
82
|
+
// Filter by date range
|
|
83
|
+
const filteredMessages = filterMessagesByDate(messages, options);
|
|
84
|
+
|
|
85
|
+
// Aggregate by date
|
|
86
|
+
const dailyMap = aggregateByDate(filteredMessages, fetcher);
|
|
87
|
+
|
|
88
|
+
// Convert to sorted array
|
|
89
|
+
let contributions = Array.from(dailyMap.values()).sort((a, b) =>
|
|
90
|
+
a.date.localeCompare(b.date)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Calculate intensity based on COST
|
|
94
|
+
contributions = calculateAllIntensities(contributions);
|
|
95
|
+
|
|
96
|
+
// Build final structure
|
|
97
|
+
const summary = calculateSummary(contributions);
|
|
98
|
+
const years = calculateYears(contributions);
|
|
99
|
+
|
|
100
|
+
const dateRange = {
|
|
101
|
+
start: contributions[0]?.date ?? "",
|
|
102
|
+
end: contributions[contributions.length - 1]?.date ?? "",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
meta: {
|
|
107
|
+
generatedAt: new Date().toISOString(),
|
|
108
|
+
version: VERSION,
|
|
109
|
+
dateRange,
|
|
110
|
+
},
|
|
111
|
+
summary,
|
|
112
|
+
years,
|
|
113
|
+
contributions,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Collect messages from all enabled sources
|
|
119
|
+
*/
|
|
120
|
+
function collectMessages(
|
|
121
|
+
options: GraphOptions,
|
|
122
|
+
fetcher: PricingFetcher
|
|
123
|
+
): UnifiedMessage[] {
|
|
124
|
+
const messages: UnifiedMessage[] = [];
|
|
125
|
+
const enabledSources = getEnabledSources(options);
|
|
126
|
+
|
|
127
|
+
// OpenCode
|
|
128
|
+
if (enabledSources.includes("opencode")) {
|
|
129
|
+
const openCodeMessages = parseOpenCodeMessages();
|
|
130
|
+
for (const msg of openCodeMessages) {
|
|
131
|
+
const pricing = fetcher.getModelPricing(msg.modelId);
|
|
132
|
+
const cost = pricing
|
|
133
|
+
? fetcher.calculateCost(
|
|
134
|
+
{
|
|
135
|
+
input: msg.tokens.input,
|
|
136
|
+
output: msg.tokens.output,
|
|
137
|
+
reasoning: msg.tokens.reasoning,
|
|
138
|
+
cacheRead: msg.tokens.cacheRead,
|
|
139
|
+
cacheWrite: msg.tokens.cacheWrite,
|
|
140
|
+
},
|
|
141
|
+
pricing
|
|
142
|
+
)
|
|
143
|
+
: 0;
|
|
144
|
+
|
|
145
|
+
messages.push({
|
|
146
|
+
source: "opencode",
|
|
147
|
+
modelId: msg.modelId,
|
|
148
|
+
providerId: msg.providerId,
|
|
149
|
+
timestamp: msg.timestamp,
|
|
150
|
+
tokens: msg.tokens,
|
|
151
|
+
cost,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Claude Code
|
|
157
|
+
if (enabledSources.includes("claude")) {
|
|
158
|
+
const claudeMessages = parseClaudeCodeMessages();
|
|
159
|
+
for (const msg of claudeMessages) {
|
|
160
|
+
const pricing = fetcher.getModelPricing(msg.modelId);
|
|
161
|
+
const cost = pricing
|
|
162
|
+
? fetcher.calculateCost(
|
|
163
|
+
{
|
|
164
|
+
input: msg.tokens.input,
|
|
165
|
+
output: msg.tokens.output,
|
|
166
|
+
cacheRead: msg.tokens.cacheRead,
|
|
167
|
+
cacheWrite: msg.tokens.cacheWrite,
|
|
168
|
+
},
|
|
169
|
+
pricing
|
|
170
|
+
)
|
|
171
|
+
: 0;
|
|
172
|
+
|
|
173
|
+
messages.push({
|
|
174
|
+
source: "claude",
|
|
175
|
+
modelId: msg.modelId,
|
|
176
|
+
providerId: msg.providerId,
|
|
177
|
+
timestamp: msg.timestamp,
|
|
178
|
+
tokens: msg.tokens,
|
|
179
|
+
cost,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Codex
|
|
185
|
+
if (enabledSources.includes("codex")) {
|
|
186
|
+
const codexMessages = parseCodexMessages();
|
|
187
|
+
for (const msg of codexMessages) {
|
|
188
|
+
const pricing = fetcher.getModelPricing(msg.modelId);
|
|
189
|
+
const cost = pricing
|
|
190
|
+
? fetcher.calculateCost(
|
|
191
|
+
{
|
|
192
|
+
input: msg.tokens.input,
|
|
193
|
+
output: msg.tokens.output,
|
|
194
|
+
cacheRead: msg.tokens.cacheRead,
|
|
195
|
+
cacheWrite: msg.tokens.cacheWrite,
|
|
196
|
+
},
|
|
197
|
+
pricing
|
|
198
|
+
)
|
|
199
|
+
: 0;
|
|
200
|
+
|
|
201
|
+
messages.push({
|
|
202
|
+
source: "codex",
|
|
203
|
+
modelId: msg.modelId,
|
|
204
|
+
providerId: msg.providerId,
|
|
205
|
+
timestamp: msg.timestamp,
|
|
206
|
+
tokens: msg.tokens,
|
|
207
|
+
cost,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Gemini
|
|
213
|
+
if (enabledSources.includes("gemini")) {
|
|
214
|
+
const geminiMessages = parseGeminiMessages();
|
|
215
|
+
for (const msg of geminiMessages) {
|
|
216
|
+
const pricing = fetcher.getModelPricing(msg.modelId);
|
|
217
|
+
// Gemini: thoughts/reasoning count as output for billing
|
|
218
|
+
const cost = pricing
|
|
219
|
+
? fetcher.calculateCost(
|
|
220
|
+
{
|
|
221
|
+
input: msg.tokens.input,
|
|
222
|
+
output: msg.tokens.output + msg.tokens.reasoning,
|
|
223
|
+
cacheRead: 0, // Gemini cached tokens are free
|
|
224
|
+
cacheWrite: 0,
|
|
225
|
+
},
|
|
226
|
+
pricing
|
|
227
|
+
)
|
|
228
|
+
: 0;
|
|
229
|
+
|
|
230
|
+
messages.push({
|
|
231
|
+
source: "gemini",
|
|
232
|
+
modelId: msg.modelId,
|
|
233
|
+
providerId: msg.providerId,
|
|
234
|
+
timestamp: msg.timestamp,
|
|
235
|
+
tokens: msg.tokens,
|
|
236
|
+
cost,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return messages;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get list of enabled sources based on options
|
|
246
|
+
*/
|
|
247
|
+
function getEnabledSources(options: GraphOptions): SourceType[] {
|
|
248
|
+
if (options.sources && options.sources.length > 0) {
|
|
249
|
+
return options.sources;
|
|
250
|
+
}
|
|
251
|
+
return ["opencode", "claude", "codex", "gemini"];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Filter messages by date range
|
|
256
|
+
*/
|
|
257
|
+
function filterMessagesByDate(
|
|
258
|
+
messages: UnifiedMessage[],
|
|
259
|
+
options: GraphOptions
|
|
260
|
+
): UnifiedMessage[] {
|
|
261
|
+
let filtered = messages;
|
|
262
|
+
|
|
263
|
+
// Filter by year
|
|
264
|
+
if (options.year) {
|
|
265
|
+
const yearStart = new Date(`${options.year}-01-01`).getTime();
|
|
266
|
+
const yearEnd = new Date(`${options.year}-12-31T23:59:59.999`).getTime();
|
|
267
|
+
filtered = filtered.filter(
|
|
268
|
+
(m) => m.timestamp >= yearStart && m.timestamp <= yearEnd
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Filter by since
|
|
273
|
+
if (options.since) {
|
|
274
|
+
const sinceTime = new Date(options.since).getTime();
|
|
275
|
+
filtered = filtered.filter((m) => m.timestamp >= sinceTime);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Filter by until
|
|
279
|
+
if (options.until) {
|
|
280
|
+
const untilTime = new Date(`${options.until}T23:59:59.999`).getTime();
|
|
281
|
+
filtered = filtered.filter((m) => m.timestamp <= untilTime);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return filtered;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Aggregate messages by date
|
|
289
|
+
*/
|
|
290
|
+
function aggregateByDate(
|
|
291
|
+
messages: UnifiedMessage[],
|
|
292
|
+
_fetcher: PricingFetcher
|
|
293
|
+
): Map<string, DailyContribution> {
|
|
294
|
+
const dailyMap = new Map<string, DailyContribution>();
|
|
295
|
+
|
|
296
|
+
for (const msg of messages) {
|
|
297
|
+
const dateKey = format(new Date(msg.timestamp), "yyyy-MM-dd");
|
|
298
|
+
|
|
299
|
+
let daily = dailyMap.get(dateKey);
|
|
300
|
+
if (!daily) {
|
|
301
|
+
daily = createEmptyDailyContribution(dateKey);
|
|
302
|
+
dailyMap.set(dateKey, daily);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Update totals
|
|
306
|
+
const totalTokens =
|
|
307
|
+
msg.tokens.input +
|
|
308
|
+
msg.tokens.output +
|
|
309
|
+
msg.tokens.cacheRead +
|
|
310
|
+
msg.tokens.cacheWrite +
|
|
311
|
+
msg.tokens.reasoning;
|
|
312
|
+
|
|
313
|
+
daily.totals.tokens += totalTokens;
|
|
314
|
+
daily.totals.cost += msg.cost;
|
|
315
|
+
daily.totals.messages += 1;
|
|
316
|
+
|
|
317
|
+
// Update token breakdown
|
|
318
|
+
daily.tokenBreakdown.input += msg.tokens.input;
|
|
319
|
+
daily.tokenBreakdown.output += msg.tokens.output;
|
|
320
|
+
daily.tokenBreakdown.cacheRead += msg.tokens.cacheRead;
|
|
321
|
+
daily.tokenBreakdown.cacheWrite += msg.tokens.cacheWrite;
|
|
322
|
+
daily.tokenBreakdown.reasoning += msg.tokens.reasoning;
|
|
323
|
+
|
|
324
|
+
// Update source contributions
|
|
325
|
+
let sourceContrib = daily.sources.find(
|
|
326
|
+
(s) => s.source === msg.source && s.modelId === msg.modelId
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (!sourceContrib) {
|
|
330
|
+
sourceContrib = {
|
|
331
|
+
source: msg.source,
|
|
332
|
+
modelId: msg.modelId,
|
|
333
|
+
providerId: msg.providerId,
|
|
334
|
+
tokens: {
|
|
335
|
+
input: 0,
|
|
336
|
+
output: 0,
|
|
337
|
+
cacheRead: 0,
|
|
338
|
+
cacheWrite: 0,
|
|
339
|
+
reasoning: 0,
|
|
340
|
+
},
|
|
341
|
+
cost: 0,
|
|
342
|
+
messages: 0,
|
|
343
|
+
};
|
|
344
|
+
daily.sources.push(sourceContrib);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
sourceContrib.tokens.input += msg.tokens.input;
|
|
348
|
+
sourceContrib.tokens.output += msg.tokens.output;
|
|
349
|
+
sourceContrib.tokens.cacheRead += msg.tokens.cacheRead;
|
|
350
|
+
sourceContrib.tokens.cacheWrite += msg.tokens.cacheWrite;
|
|
351
|
+
sourceContrib.tokens.reasoning += msg.tokens.reasoning;
|
|
352
|
+
sourceContrib.cost += msg.cost;
|
|
353
|
+
sourceContrib.messages += 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return dailyMap;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Create empty daily contribution
|
|
361
|
+
*/
|
|
362
|
+
function createEmptyDailyContribution(date: string): DailyContribution {
|
|
363
|
+
return {
|
|
364
|
+
date,
|
|
365
|
+
totals: {
|
|
366
|
+
tokens: 0,
|
|
367
|
+
cost: 0,
|
|
368
|
+
messages: 0,
|
|
369
|
+
},
|
|
370
|
+
intensity: 0,
|
|
371
|
+
tokenBreakdown: {
|
|
372
|
+
input: 0,
|
|
373
|
+
output: 0,
|
|
374
|
+
cacheRead: 0,
|
|
375
|
+
cacheWrite: 0,
|
|
376
|
+
reasoning: 0,
|
|
377
|
+
},
|
|
378
|
+
sources: [],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Calculate intensity for all contributions based on COST
|
|
384
|
+
*/
|
|
385
|
+
function calculateAllIntensities(
|
|
386
|
+
contributions: DailyContribution[]
|
|
387
|
+
): DailyContribution[] {
|
|
388
|
+
if (contributions.length === 0) return contributions;
|
|
389
|
+
|
|
390
|
+
// Find max cost for intensity calculation
|
|
391
|
+
const maxCost = Math.max(...contributions.map((c) => c.totals.cost));
|
|
392
|
+
|
|
393
|
+
return contributions.map((c) => ({
|
|
394
|
+
...c,
|
|
395
|
+
intensity: calculateIntensity(c.totals.cost, maxCost),
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Calculate intensity grade based on cost ratio
|
|
401
|
+
* 0 = no activity, 4 = highest
|
|
402
|
+
*/
|
|
403
|
+
function calculateIntensity(
|
|
404
|
+
cost: number,
|
|
405
|
+
maxCost: number
|
|
406
|
+
): 0 | 1 | 2 | 3 | 4 {
|
|
407
|
+
if (cost === 0 || maxCost === 0) return 0;
|
|
408
|
+
const ratio = cost / maxCost;
|
|
409
|
+
if (ratio >= 0.75) return 4;
|
|
410
|
+
if (ratio >= 0.5) return 3;
|
|
411
|
+
if (ratio >= 0.25) return 2;
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Calculate summary statistics
|
|
417
|
+
*/
|
|
418
|
+
function calculateSummary(contributions: DailyContribution[]): DataSummary {
|
|
419
|
+
const totalTokens = contributions.reduce((sum, c) => sum + c.totals.tokens, 0);
|
|
420
|
+
const totalCost = contributions.reduce((sum, c) => sum + c.totals.cost, 0);
|
|
421
|
+
const activeDays = contributions.filter((c) => c.totals.cost > 0).length;
|
|
422
|
+
const maxCostInSingleDay = Math.max(
|
|
423
|
+
...contributions.map((c) => c.totals.cost),
|
|
424
|
+
0
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Collect unique sources and models
|
|
428
|
+
const sourcesSet = new Set<SourceType>();
|
|
429
|
+
const modelsSet = new Set<string>();
|
|
430
|
+
|
|
431
|
+
for (const c of contributions) {
|
|
432
|
+
for (const s of c.sources) {
|
|
433
|
+
sourcesSet.add(s.source);
|
|
434
|
+
modelsSet.add(s.modelId);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
totalTokens,
|
|
440
|
+
totalCost,
|
|
441
|
+
totalDays: contributions.length,
|
|
442
|
+
activeDays,
|
|
443
|
+
averagePerDay: activeDays > 0 ? totalCost / activeDays : 0,
|
|
444
|
+
maxCostInSingleDay,
|
|
445
|
+
sources: Array.from(sourcesSet),
|
|
446
|
+
models: Array.from(modelsSet),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Calculate year summaries
|
|
452
|
+
*/
|
|
453
|
+
function calculateYears(contributions: DailyContribution[]): YearSummary[] {
|
|
454
|
+
const yearsMap = new Map<
|
|
455
|
+
string,
|
|
456
|
+
{ tokens: number; cost: number; start: string; end: string }
|
|
457
|
+
>();
|
|
458
|
+
|
|
459
|
+
for (const c of contributions) {
|
|
460
|
+
const year = c.date.substring(0, 4);
|
|
461
|
+
let yearData = yearsMap.get(year);
|
|
462
|
+
|
|
463
|
+
if (!yearData) {
|
|
464
|
+
yearData = { tokens: 0, cost: 0, start: c.date, end: c.date };
|
|
465
|
+
yearsMap.set(year, yearData);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
yearData.tokens += c.totals.tokens;
|
|
469
|
+
yearData.cost += c.totals.cost;
|
|
470
|
+
if (c.date < yearData.start) yearData.start = c.date;
|
|
471
|
+
if (c.date > yearData.end) yearData.end = c.date;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return Array.from(yearsMap.entries())
|
|
475
|
+
.map(([year, data]) => ({
|
|
476
|
+
year,
|
|
477
|
+
totalTokens: data.tokens,
|
|
478
|
+
totalCost: data.cost,
|
|
479
|
+
range: {
|
|
480
|
+
start: data.start,
|
|
481
|
+
end: data.end,
|
|
482
|
+
},
|
|
483
|
+
}))
|
|
484
|
+
.sort((a, b) => a.year.localeCompare(b.year));
|
|
485
|
+
}
|