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.
@@ -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?.providerData?.rawUsage;
84
+ const usage = normalizeUsageFromRecord(record);
84
85
  const timestamp = record?.timestamp;
85
86
  if (!usage || timestamp == null)
86
87
  continue;
@@ -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
- if (cost > topModelCost) {
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] = (mAgg.modelCost[modelId] ?? 0) + cost;
268
- mAgg.projectCost[projectName] = (mAgg.projectCost[projectName] ?? 0) + cost;
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] = (pAgg.modelCost[modelId] ?? 0) + cost;
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?.providerData?.rawUsage;
426
+ const usage = normalizeUsageFromRecord(record);
415
427
  const timestamp = record?.timestamp;
416
428
  if (!usage || timestamp == null)
417
429
  continue;
@@ -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
+ }
@@ -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
- if (c > topModel.cost) {
50
- topModel = { id: model, cost: c };
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
- for (const [modelId, c] of Object.entries(agg.modelCost)) {
24
- if (c > topModelCost) {
25
- topModelCost = c;
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] = (p.modelCost[modelId] ?? 0) + cost;
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
- for (const [modelId, c] of Object.entries(p.modelCost)) {
83
- if (c > topModelCost) {
84
- topModelCost = c;
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
  }
@@ -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] = (monthAgg.modelCost[modelId] ?? 0) + cost;
34
- monthAgg.projectCost[projectName] = (monthAgg.projectCost[projectName] ?? 0) + cost;
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, c] of Object.entries(m.modelCost)) {
71
- if (c > topModel.cost)
72
- topModel = { id: modelId, cost: c };
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, c] of Object.entries(m.projectCost)) {
75
- if (c > topProject.cost)
76
- topProject = { name: projectName, cost: c };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebuddy-stats",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [