codebuddy-stats 1.4.1 → 1.4.3
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/lib/code-usage.worker.js +2 -1
- package/dist/lib/data-loader.js +20 -8
- package/dist/lib/pricing.js +2 -1
- package/dist/lib/utils.js +52 -0
- package/dist/views/daily.js +7 -4
- package/dist/views/monthly-detail.js +17 -7
- package/dist/views/monthly.js +21 -11
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
6
6
|
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js';
|
|
7
|
+
import { normalizeUsageFromRecord } from './utils.js';
|
|
7
8
|
function getProjectName(filePath) {
|
|
8
9
|
const parts = filePath.split(path.sep);
|
|
9
10
|
const projectsIndex = parts.lastIndexOf('projects');
|
|
@@ -80,7 +81,7 @@ async function main() {
|
|
|
80
81
|
for await (const line of rl) {
|
|
81
82
|
try {
|
|
82
83
|
const record = JSON.parse(line);
|
|
83
|
-
const usage = record
|
|
84
|
+
const usage = normalizeUsageFromRecord(record);
|
|
84
85
|
const timestamp = record?.timestamp;
|
|
85
86
|
if (!usage || timestamp == null)
|
|
86
87
|
continue;
|
package/dist/lib/data-loader.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createInterface } from 'node:readline';
|
|
|
8
8
|
import { Worker } from 'node:worker_threads';
|
|
9
9
|
import { getIdeDataDir, getProjectsDir, getSettingsPath } from './paths.js';
|
|
10
10
|
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js';
|
|
11
|
-
import { compareByCostThenTokens } from './utils.js';
|
|
11
|
+
import { compareByCostThenTokens, normalizeUsageFromRecord, } from './utils.js';
|
|
12
12
|
import { loadWorkspaceMappings, resolveProjectName } from './workspace-resolver.js';
|
|
13
13
|
export const BASE_DIR = getProjectsDir();
|
|
14
14
|
const memoryCache = new Map();
|
|
@@ -225,8 +225,10 @@ function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals,
|
|
|
225
225
|
const daySummary = { cost: 0, tokens: 0, requests: 0 };
|
|
226
226
|
let topModelId = '-';
|
|
227
227
|
let topModelCost = 0;
|
|
228
|
+
let topModelTokens = 0;
|
|
228
229
|
let topProjectName = '-';
|
|
229
230
|
let topProjectCost = 0;
|
|
231
|
+
let topProjectTokens = 0;
|
|
230
232
|
const month = date.slice(0, 7);
|
|
231
233
|
if (month) {
|
|
232
234
|
monthlyAgg[month] ??= {
|
|
@@ -242,6 +244,7 @@ function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals,
|
|
|
242
244
|
}
|
|
243
245
|
for (const [projectName, models] of Object.entries(dayData ?? {})) {
|
|
244
246
|
let projectCost = 0;
|
|
247
|
+
let projectTokens = 0;
|
|
245
248
|
if (month) {
|
|
246
249
|
monthlyByProject[month][projectName] ??= { cost: 0, tokens: 0, requests: 0, modelCost: {} };
|
|
247
250
|
}
|
|
@@ -253,8 +256,10 @@ function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals,
|
|
|
253
256
|
daySummary.tokens += tokens;
|
|
254
257
|
daySummary.requests += requests;
|
|
255
258
|
projectCost += cost;
|
|
256
|
-
|
|
259
|
+
projectTokens += tokens;
|
|
260
|
+
if (cost > topModelCost || (cost === topModelCost && tokens > topModelTokens)) {
|
|
257
261
|
topModelCost = cost;
|
|
262
|
+
topModelTokens = tokens;
|
|
258
263
|
topModelId = modelId;
|
|
259
264
|
}
|
|
260
265
|
if (month) {
|
|
@@ -264,22 +269,29 @@ function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals,
|
|
|
264
269
|
mAgg.requests += requests;
|
|
265
270
|
mAgg.cacheHitTokens += stats.cacheHitTokens;
|
|
266
271
|
mAgg.cacheMissTokens += stats.cacheMissTokens;
|
|
267
|
-
mAgg.modelCost[modelId]
|
|
268
|
-
mAgg.
|
|
272
|
+
mAgg.modelCost[modelId] ??= { cost: 0, tokens: 0 };
|
|
273
|
+
mAgg.modelCost[modelId].cost += cost;
|
|
274
|
+
mAgg.modelCost[modelId].tokens += tokens;
|
|
275
|
+
mAgg.projectCost[projectName] ??= { cost: 0, tokens: 0 };
|
|
276
|
+
mAgg.projectCost[projectName].cost += cost;
|
|
277
|
+
mAgg.projectCost[projectName].tokens += tokens;
|
|
269
278
|
const pAgg = monthlyByProject[month][projectName];
|
|
270
279
|
pAgg.cost += cost;
|
|
271
280
|
pAgg.tokens += tokens;
|
|
272
281
|
pAgg.requests += requests;
|
|
273
|
-
pAgg.modelCost[modelId]
|
|
282
|
+
pAgg.modelCost[modelId] ??= { cost: 0, tokens: 0 };
|
|
283
|
+
pAgg.modelCost[modelId].cost += cost;
|
|
284
|
+
pAgg.modelCost[modelId].tokens += tokens;
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
|
-
if (projectCost > topProjectCost) {
|
|
287
|
+
if (projectCost > topProjectCost || (projectCost === topProjectCost && projectTokens > topProjectTokens)) {
|
|
277
288
|
topProjectCost = projectCost;
|
|
289
|
+
topProjectTokens = projectTokens;
|
|
278
290
|
topProjectName = projectName;
|
|
279
291
|
}
|
|
280
292
|
}
|
|
281
293
|
dailySummary[date] = daySummary;
|
|
282
|
-
dailyTops[date] = { topModelId, topModelCost, topProjectName, topProjectCost };
|
|
294
|
+
dailyTops[date] = { topModelId, topModelCost, topModelTokens, topProjectName, topProjectCost, topProjectTokens };
|
|
283
295
|
}
|
|
284
296
|
const projectDisplayNames = {};
|
|
285
297
|
for (const projectName of Object.keys(projectTotals)) {
|
|
@@ -411,7 +423,7 @@ async function loadCodeUsageData(options = {}) {
|
|
|
411
423
|
for await (const line of rl) {
|
|
412
424
|
try {
|
|
413
425
|
const record = JSON.parse(line);
|
|
414
|
-
const usage = record
|
|
426
|
+
const usage = normalizeUsageFromRecord(record);
|
|
415
427
|
const timestamp = record?.timestamp;
|
|
416
428
|
if (!usage || timestamp == null)
|
|
417
429
|
continue;
|
package/dist/lib/pricing.js
CHANGED
|
@@ -106,7 +106,7 @@ export const MODEL_PRICING = {
|
|
|
106
106
|
cacheWrite: [
|
|
107
107
|
{ limit: 200_000, pricePerMTok: 0.2 },
|
|
108
108
|
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
|
|
109
|
-
]
|
|
109
|
+
],
|
|
110
110
|
},
|
|
111
111
|
"gemini-3.0-pro": {
|
|
112
112
|
prompt: [
|
|
@@ -126,6 +126,7 @@ export const MODEL_PRICING = {
|
|
|
126
126
|
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
|
|
127
127
|
],
|
|
128
128
|
},
|
|
129
|
+
"gemini-3.1-flash-lite": createPricing(0.25, 0.03, 1.5),
|
|
129
130
|
"gemini-3.0-flash": createPricing(0.5, 0.05, 3.0),
|
|
130
131
|
"gemini-2.5-pro": {
|
|
131
132
|
prompt: [
|
package/dist/lib/utils.js
CHANGED
|
@@ -65,3 +65,55 @@ export function compareByCostThenTokens(a, b) {
|
|
|
65
65
|
return costDiff;
|
|
66
66
|
return b[1].tokens - a[1].tokens;
|
|
67
67
|
}
|
|
68
|
+
function toNonNegativeNumber(value) {
|
|
69
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
70
|
+
return null;
|
|
71
|
+
return Math.max(0, value);
|
|
72
|
+
}
|
|
73
|
+
function sumCachedTokensFromDetails(details) {
|
|
74
|
+
if (!Array.isArray(details))
|
|
75
|
+
return 0;
|
|
76
|
+
let total = 0;
|
|
77
|
+
for (const item of details) {
|
|
78
|
+
if (!item || typeof item !== 'object')
|
|
79
|
+
continue;
|
|
80
|
+
const maybeCachedTokens = item.cached_tokens ??
|
|
81
|
+
item.cachedTokens;
|
|
82
|
+
const cachedTokens = toNonNegativeNumber(maybeCachedTokens);
|
|
83
|
+
if (cachedTokens != null)
|
|
84
|
+
total += cachedTokens;
|
|
85
|
+
}
|
|
86
|
+
return total;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 统一将不同来源的 usage 结构归一为 RawUsage
|
|
90
|
+
* 优先使用 providerData.rawUsage,否则回退到 providerData.usage / message.usage
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeUsageFromRecord(record) {
|
|
93
|
+
const rawUsage = record?.providerData?.rawUsage;
|
|
94
|
+
if (rawUsage)
|
|
95
|
+
return rawUsage;
|
|
96
|
+
const usage = (record?.providerData?.usage ?? record?.message?.usage);
|
|
97
|
+
if (!usage || typeof usage !== 'object')
|
|
98
|
+
return null;
|
|
99
|
+
const inputTokens = toNonNegativeNumber(usage.inputTokens);
|
|
100
|
+
const outputTokens = toNonNegativeNumber(usage.outputTokens);
|
|
101
|
+
const totalTokens = toNonNegativeNumber(usage.totalTokens);
|
|
102
|
+
const cacheHitTokens = sumCachedTokensFromDetails(usage.inputTokensDetails);
|
|
103
|
+
if (inputTokens == null && outputTokens == null && totalTokens == null && cacheHitTokens <= 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const normalizedPromptTokens = inputTokens ?? Math.max((totalTokens ?? 0) - (outputTokens ?? 0), 0);
|
|
107
|
+
const normalizedCompletionTokens = outputTokens ?? Math.max((totalTokens ?? 0) - normalizedPromptTokens, 0);
|
|
108
|
+
const normalizedTotalTokens = totalTokens ?? normalizedPromptTokens + normalizedCompletionTokens;
|
|
109
|
+
const normalizedCacheHitTokens = Math.min(cacheHitTokens, normalizedPromptTokens);
|
|
110
|
+
const normalizedCacheMissTokens = Math.max(normalizedPromptTokens - normalizedCacheHitTokens, 0);
|
|
111
|
+
return {
|
|
112
|
+
prompt_tokens: normalizedPromptTokens,
|
|
113
|
+
completion_tokens: normalizedCompletionTokens,
|
|
114
|
+
total_tokens: normalizedTotalTokens,
|
|
115
|
+
prompt_cache_hit_tokens: normalizedCacheHitTokens,
|
|
116
|
+
prompt_cache_miss_tokens: normalizedCacheMissTokens,
|
|
117
|
+
cache_creation_input_tokens: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/views/daily.js
CHANGED
|
@@ -42,16 +42,19 @@ export function renderDaily(box, data, scrollOffset = 0, selectedIndex = 0, widt
|
|
|
42
42
|
else {
|
|
43
43
|
for (const [project, models] of Object.entries(dayData)) {
|
|
44
44
|
let projectCost = 0;
|
|
45
|
+
let projectTokens = 0;
|
|
45
46
|
for (const [model, stats] of Object.entries(models)) {
|
|
46
47
|
const modelStats = stats;
|
|
47
48
|
const c = Number(modelStats.cost ?? 0);
|
|
49
|
+
const t = Number(modelStats.totalTokens ?? 0);
|
|
48
50
|
projectCost += c;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
projectTokens += t;
|
|
52
|
+
if (c > topModel.cost || (c === topModel.cost && t > (topModel.tokens ?? 0))) {
|
|
53
|
+
topModel = { id: model, cost: c, tokens: t };
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
|
-
if (projectCost > topProject.cost) {
|
|
54
|
-
topProject = { name: project, cost: projectCost };
|
|
56
|
+
if (projectCost > topProject.cost || (projectCost === topProject.cost && projectTokens > (topProject.tokens ?? 0))) {
|
|
57
|
+
topProject = { name: project, cost: projectCost, tokens: projectTokens };
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -20,9 +20,13 @@ export function renderMonthlyDetail(box, data, month, scrollOffset = 0, width, p
|
|
|
20
20
|
.map(([projectName, agg]) => {
|
|
21
21
|
let topModelId = '-';
|
|
22
22
|
let topModelCost = 0;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
let topModelTokens = 0;
|
|
24
|
+
for (const [modelId, modelStats] of Object.entries(agg.modelCost)) {
|
|
25
|
+
const cost = modelStats.cost;
|
|
26
|
+
const tokens = modelStats.tokens;
|
|
27
|
+
if (cost > topModelCost || (cost === topModelCost && tokens > topModelTokens)) {
|
|
28
|
+
topModelCost = cost;
|
|
29
|
+
topModelTokens = tokens;
|
|
26
30
|
topModelId = modelId;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
@@ -66,7 +70,9 @@ export function renderMonthlyDetail(box, data, month, scrollOffset = 0, width, p
|
|
|
66
70
|
p.cost += cost;
|
|
67
71
|
p.tokens += tokens;
|
|
68
72
|
p.requests += requests;
|
|
69
|
-
p.modelCost[modelId]
|
|
73
|
+
p.modelCost[modelId] ??= { cost: 0, tokens: 0 };
|
|
74
|
+
p.modelCost[modelId].cost += cost;
|
|
75
|
+
p.modelCost[modelId].tokens += tokens;
|
|
70
76
|
totalCost += cost;
|
|
71
77
|
totalTokens += tokens;
|
|
72
78
|
totalRequests += requests;
|
|
@@ -79,9 +85,13 @@ export function renderMonthlyDetail(box, data, month, scrollOffset = 0, width, p
|
|
|
79
85
|
.map(p => {
|
|
80
86
|
let topModelId = '-';
|
|
81
87
|
let topModelCost = 0;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let topModelTokens = 0;
|
|
89
|
+
for (const [modelId, modelStats] of Object.entries(p.modelCost)) {
|
|
90
|
+
const cost = modelStats.cost;
|
|
91
|
+
const tokens = modelStats.tokens;
|
|
92
|
+
if (cost > topModelCost || (cost === topModelCost && tokens > topModelTokens)) {
|
|
93
|
+
topModelCost = cost;
|
|
94
|
+
topModelTokens = tokens;
|
|
85
95
|
topModelId = modelId;
|
|
86
96
|
}
|
|
87
97
|
}
|
package/dist/views/monthly.js
CHANGED
|
@@ -30,8 +30,12 @@ export function renderMonthly(box, data, scrollOffset = 0, selectedIndex = 0, wi
|
|
|
30
30
|
monthAgg.requests += requests;
|
|
31
31
|
monthAgg.cacheHitTokens += cacheHit;
|
|
32
32
|
monthAgg.cacheMissTokens += cacheMiss;
|
|
33
|
-
monthAgg.modelCost[modelId]
|
|
34
|
-
monthAgg.
|
|
33
|
+
monthAgg.modelCost[modelId] ??= { cost: 0, tokens: 0 };
|
|
34
|
+
monthAgg.modelCost[modelId].cost += cost;
|
|
35
|
+
monthAgg.modelCost[modelId].tokens += tokens;
|
|
36
|
+
monthAgg.projectCost[projectName] ??= { cost: 0, tokens: 0 };
|
|
37
|
+
monthAgg.projectCost[projectName].cost += cost;
|
|
38
|
+
monthAgg.projectCost[projectName].tokens += tokens;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
}
|
|
@@ -64,16 +68,22 @@ export function renderMonthly(box, data, scrollOffset = 0, selectedIndex = 0, wi
|
|
|
64
68
|
const m = aggByMonth[month];
|
|
65
69
|
if (!m)
|
|
66
70
|
continue;
|
|
67
|
-
// top model / project by cost
|
|
68
|
-
let topModel = { id: '-', cost: 0 };
|
|
69
|
-
let topProject = { name: '-', cost: 0 };
|
|
70
|
-
for (const [modelId,
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// top model / project by cost (cost 相等时比较 tokens)
|
|
72
|
+
let topModel = { id: '-', cost: 0, tokens: 0 };
|
|
73
|
+
let topProject = { name: '-', cost: 0, tokens: 0 };
|
|
74
|
+
for (const [modelId, modelStats] of Object.entries(m.modelCost)) {
|
|
75
|
+
const cost = modelStats.cost;
|
|
76
|
+
const tokens = modelStats.tokens;
|
|
77
|
+
if (cost > topModel.cost || (cost === topModel.cost && tokens > topModel.tokens)) {
|
|
78
|
+
topModel = { id: modelId, cost, tokens };
|
|
79
|
+
}
|
|
73
80
|
}
|
|
74
|
-
for (const [projectName,
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
for (const [projectName, projectStats] of Object.entries(m.projectCost)) {
|
|
82
|
+
const cost = projectStats.cost;
|
|
83
|
+
const tokens = projectStats.tokens;
|
|
84
|
+
if (cost > topProject.cost || (cost === topProject.cost && tokens > topProject.tokens)) {
|
|
85
|
+
topProject = { name: projectName, cost, tokens };
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
88
|
const shortProject = data.projectDisplayNames?.[topProject.name] ?? resolveProjectName(topProject.name, data.workspaceMappings);
|
|
79
89
|
const isSelected = scrollOffset + i === selectedIndex;
|