vibeusage 0.2.9 → 0.2.11
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/README.md +12 -0
- package/README.zh-CN.md +12 -0
- package/package.json +3 -1
- package/src/lib/insforge-client.js +10 -1
- package/src/lib/opencode-usage-audit.js +202 -0
- package/src/lib/rollout.js +103 -13
package/README.md
CHANGED
|
@@ -91,6 +91,8 @@ npx --yes vibeusage status
|
|
|
91
91
|
|
|
92
92
|
- `VIBESCORE_HTTP_TIMEOUT_MS`: CLI HTTP timeout in ms (default `20000`, `0` disables, clamped to `1000..120000`).
|
|
93
93
|
- `VITE_VIBESCORE_HTTP_TIMEOUT_MS`: Dashboard request timeout in ms (default `15000`, `0` disables, clamped to `1000..30000`).
|
|
94
|
+
- `VIBEUSAGE_ROLLUP_ENABLED`: Currently ignored; rollup aggregation is disabled in code until the daily rollup table is deployed.
|
|
95
|
+
- `VIBESCORE_ROLLUP_ENABLED`: Legacy alias for `VIBEUSAGE_ROLLUP_ENABLED` (ignored).
|
|
94
96
|
- `GEMINI_HOME`: Override Gemini CLI home (defaults to `~/.gemini`).
|
|
95
97
|
|
|
96
98
|
## 🧰 Troubleshooting
|
|
@@ -170,6 +172,16 @@ npm run validate:copy
|
|
|
170
172
|
npm run smoke
|
|
171
173
|
```
|
|
172
174
|
|
|
175
|
+
### Architecture Canvas Focus
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Generate a focused canvas for a top-level module
|
|
179
|
+
node scripts/ops/architecture-canvas.cjs --focus src
|
|
180
|
+
|
|
181
|
+
# Alias: --module
|
|
182
|
+
node scripts/ops/architecture-canvas.cjs --module dashboard
|
|
183
|
+
```
|
|
184
|
+
|
|
173
185
|
## 📜 License
|
|
174
186
|
|
|
175
187
|
This project is licensed under the [MIT License](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -91,6 +91,8 @@ npx --yes vibeusage status
|
|
|
91
91
|
|
|
92
92
|
- `VIBESCORE_HTTP_TIMEOUT_MS`:CLI 请求超时(毫秒,默认 `20000`,`0` 表示关闭,范围 `1000..120000`)。
|
|
93
93
|
- `VITE_VIBESCORE_HTTP_TIMEOUT_MS`:Dashboard 请求超时(毫秒,默认 `15000`,`0` 表示关闭,范围 `1000..30000`)。
|
|
94
|
+
- `VIBEUSAGE_ROLLUP_ENABLED`:当前被忽略,rollup 聚合在代码层禁用,等待 rollup 表部署完成后再恢复。
|
|
95
|
+
- `VIBESCORE_ROLLUP_ENABLED`:`VIBEUSAGE_ROLLUP_ENABLED` 的兼容别名(同样无效)。
|
|
94
96
|
- `GEMINI_HOME`:覆盖 Gemini CLI 的 home(默认 `~/.gemini`)。
|
|
95
97
|
|
|
96
98
|
## 🧰 常见问题
|
|
@@ -170,6 +172,16 @@ npm run validate:copy
|
|
|
170
172
|
npm run smoke
|
|
171
173
|
```
|
|
172
174
|
|
|
175
|
+
### 架构画布聚焦
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# 生成指定顶层模块的聚焦画布
|
|
179
|
+
node scripts/ops/architecture-canvas.cjs --focus src
|
|
180
|
+
|
|
181
|
+
# 别名:--module
|
|
182
|
+
node scripts/ops/architecture-canvas.cjs --module dashboard
|
|
183
|
+
```
|
|
184
|
+
|
|
173
185
|
## 📜 开源协议
|
|
174
186
|
|
|
175
187
|
本项目基于 [MIT](LICENSE) 协议开源。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"copy:pull": "node scripts/copy-sync.cjs pull",
|
|
21
21
|
"copy:push": "node scripts/copy-sync.cjs push",
|
|
22
22
|
"architecture:canvas": "node scripts/ops/architecture-canvas.cjs",
|
|
23
|
+
"architecture:canvas:focus": "node scripts/ops/architecture-canvas.cjs --focus",
|
|
24
|
+
"architecture:canvas:list-modules": "node scripts/ops/architecture-canvas.cjs --list-modules",
|
|
23
25
|
"validate:guardrails": "node scripts/validate-architecture-guardrails.cjs"
|
|
24
26
|
},
|
|
25
27
|
"bin": {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function loadInsforgeSdk() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@insforge/sdk');
|
|
6
|
+
} catch (err) {
|
|
7
|
+
const wrapped = new Error('Missing dependency @insforge/sdk. Please reinstall vibeusage.');
|
|
8
|
+
wrapped.cause = err;
|
|
9
|
+
throw wrapped;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
function getAnonKey({ env = process.env } = {}) {
|
|
6
14
|
return (
|
|
@@ -44,6 +52,7 @@ function createTimeoutFetch(baseFetch) {
|
|
|
44
52
|
|
|
45
53
|
function createInsforgeClient({ baseUrl, accessToken } = {}) {
|
|
46
54
|
if (!baseUrl) throw new Error('Missing baseUrl');
|
|
55
|
+
const { createClient } = loadInsforgeSdk();
|
|
47
56
|
const anonKey = getAnonKey();
|
|
48
57
|
return createClient({
|
|
49
58
|
baseUrl,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { listOpencodeMessageFiles, parseOpencodeIncremental } = require('./rollout');
|
|
6
|
+
|
|
7
|
+
const BUCKET_SEPARATOR = '|';
|
|
8
|
+
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
|
|
10
|
+
function formatHourKey(date) {
|
|
11
|
+
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(
|
|
12
|
+
date.getUTCDate()
|
|
13
|
+
).padStart(2, '0')}T${String(date.getUTCHours()).padStart(2, '0')}:${String(
|
|
14
|
+
date.getUTCMinutes() >= 30 ? 30 : 0
|
|
15
|
+
).padStart(2, '0')}:00`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toBig(value) {
|
|
19
|
+
if (typeof value === 'bigint') return value;
|
|
20
|
+
if (typeof value === 'number') return BigInt(value);
|
|
21
|
+
if (typeof value === 'string' && value.trim()) return BigInt(value);
|
|
22
|
+
return 0n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addTotals(target, delta) {
|
|
26
|
+
target.input_tokens += toBig(delta.input_tokens);
|
|
27
|
+
target.cached_input_tokens += toBig(delta.cached_input_tokens);
|
|
28
|
+
target.output_tokens += toBig(delta.output_tokens);
|
|
29
|
+
target.reasoning_output_tokens += toBig(delta.reasoning_output_tokens);
|
|
30
|
+
target.total_tokens += toBig(delta.total_tokens);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function buildLocalHourlyTotals({ storageDir, source = 'opencode' }) {
|
|
34
|
+
const messageFiles = await listOpencodeMessageFiles(storageDir);
|
|
35
|
+
const queuePath = path.join(os.tmpdir(), `vibeusage-opencode-audit-${process.pid}-${Date.now()}.jsonl`);
|
|
36
|
+
const cursors = { version: 1, files: {}, hourly: null, opencode: null };
|
|
37
|
+
|
|
38
|
+
await parseOpencodeIncremental({ messageFiles, cursors, queuePath, source });
|
|
39
|
+
await fs.rm(queuePath, { force: true }).catch(() => {});
|
|
40
|
+
|
|
41
|
+
const byHour = new Map();
|
|
42
|
+
let minDay = null;
|
|
43
|
+
let maxDay = null;
|
|
44
|
+
|
|
45
|
+
for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
|
|
46
|
+
const [bucketSource, , hourStart] = String(key).split(BUCKET_SEPARATOR);
|
|
47
|
+
if (bucketSource !== source || !hourStart) continue;
|
|
48
|
+
const dt = new Date(hourStart);
|
|
49
|
+
if (!Number.isFinite(dt.getTime())) continue;
|
|
50
|
+
const hourKey = formatHourKey(dt);
|
|
51
|
+
const dayKey = hourKey.slice(0, 10);
|
|
52
|
+
|
|
53
|
+
if (!minDay || dayKey < minDay) minDay = dayKey;
|
|
54
|
+
if (!maxDay || dayKey > maxDay) maxDay = dayKey;
|
|
55
|
+
|
|
56
|
+
const totals = byHour.get(hourKey) || {
|
|
57
|
+
input_tokens: 0n,
|
|
58
|
+
cached_input_tokens: 0n,
|
|
59
|
+
output_tokens: 0n,
|
|
60
|
+
reasoning_output_tokens: 0n,
|
|
61
|
+
total_tokens: 0n
|
|
62
|
+
};
|
|
63
|
+
addTotals(totals, bucket.totals || {});
|
|
64
|
+
byHour.set(hourKey, totals);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { byHour, minDay, maxDay };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeServerRows(rows) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for (const row of rows || []) {
|
|
73
|
+
if (!row || !row.hour) continue;
|
|
74
|
+
map.set(row.hour, {
|
|
75
|
+
missing: Boolean(row.missing),
|
|
76
|
+
totals: {
|
|
77
|
+
input_tokens: toBig(row.input_tokens),
|
|
78
|
+
cached_input_tokens: toBig(row.cached_input_tokens),
|
|
79
|
+
output_tokens: toBig(row.output_tokens),
|
|
80
|
+
reasoning_output_tokens: toBig(row.reasoning_output_tokens),
|
|
81
|
+
total_tokens: toBig(row.total_tokens)
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return map;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function diffTotals(local, server) {
|
|
89
|
+
return {
|
|
90
|
+
input_tokens: local.input_tokens - server.input_tokens,
|
|
91
|
+
cached_input_tokens: local.cached_input_tokens - server.cached_input_tokens,
|
|
92
|
+
output_tokens: local.output_tokens - server.output_tokens,
|
|
93
|
+
reasoning_output_tokens: local.reasoning_output_tokens - server.reasoning_output_tokens,
|
|
94
|
+
total_tokens: local.total_tokens - server.total_tokens
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function maxAbsDelta(delta) {
|
|
99
|
+
return [
|
|
100
|
+
delta.input_tokens,
|
|
101
|
+
delta.cached_input_tokens,
|
|
102
|
+
delta.output_tokens,
|
|
103
|
+
delta.reasoning_output_tokens,
|
|
104
|
+
delta.total_tokens
|
|
105
|
+
].reduce((acc, value) => {
|
|
106
|
+
const abs = value < 0n ? -value : value;
|
|
107
|
+
return abs > acc ? abs : acc;
|
|
108
|
+
}, 0n);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isValidDay(value) {
|
|
112
|
+
if (!DAY_RE.test(value)) return false;
|
|
113
|
+
const dt = new Date(`${value}T00:00:00.000Z`);
|
|
114
|
+
return Number.isFinite(dt.getTime());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function listDays(from, to) {
|
|
118
|
+
if (!isValidDay(from) || !isValidDay(to)) return [];
|
|
119
|
+
if (from > to) return [];
|
|
120
|
+
const out = [];
|
|
121
|
+
const start = new Date(`${from}T00:00:00.000Z`);
|
|
122
|
+
const end = new Date(`${to}T00:00:00.000Z`);
|
|
123
|
+
for (let dt = start; dt <= end; dt = new Date(dt.getTime() + 24 * 60 * 60 * 1000)) {
|
|
124
|
+
const day = `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(
|
|
125
|
+
dt.getUTCDate()
|
|
126
|
+
).padStart(2, '0')}`;
|
|
127
|
+
out.push(day);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function auditOpencodeUsage({ storageDir, from, to, fetchHourly, includeMissing = false }) {
|
|
133
|
+
const local = await buildLocalHourlyTotals({ storageDir, source: 'opencode' });
|
|
134
|
+
if (!local.minDay || !local.maxDay) {
|
|
135
|
+
throw new Error('No local opencode data found');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fromDay = from || local.minDay;
|
|
139
|
+
const toDay = to || local.maxDay;
|
|
140
|
+
const days = listDays(fromDay, toDay);
|
|
141
|
+
if (days.length === 0) {
|
|
142
|
+
throw new Error('Invalid date range for audit');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const diffs = [];
|
|
146
|
+
let matched = 0;
|
|
147
|
+
let mismatched = 0;
|
|
148
|
+
let incomplete = 0;
|
|
149
|
+
let maxDelta = 0n;
|
|
150
|
+
|
|
151
|
+
for (const day of days) {
|
|
152
|
+
const server = await fetchHourly(day);
|
|
153
|
+
const serverByHour = normalizeServerRows(server?.data || []);
|
|
154
|
+
for (let h = 0; h < 24; h++) {
|
|
155
|
+
for (const m of [0, 30]) {
|
|
156
|
+
const hourKey = `${day}T${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`;
|
|
157
|
+
const localTotals = local.byHour.get(hourKey) || {
|
|
158
|
+
input_tokens: 0n,
|
|
159
|
+
cached_input_tokens: 0n,
|
|
160
|
+
output_tokens: 0n,
|
|
161
|
+
reasoning_output_tokens: 0n,
|
|
162
|
+
total_tokens: 0n
|
|
163
|
+
};
|
|
164
|
+
const serverEntry = serverByHour.get(hourKey) || null;
|
|
165
|
+
if (serverEntry?.missing && !includeMissing) {
|
|
166
|
+
incomplete += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const serverTotals = serverEntry?.totals || {
|
|
170
|
+
input_tokens: 0n,
|
|
171
|
+
cached_input_tokens: 0n,
|
|
172
|
+
output_tokens: 0n,
|
|
173
|
+
reasoning_output_tokens: 0n,
|
|
174
|
+
total_tokens: 0n
|
|
175
|
+
};
|
|
176
|
+
const delta = diffTotals(localTotals, serverTotals);
|
|
177
|
+
const deltaMax = maxAbsDelta(delta);
|
|
178
|
+
if (deltaMax === 0n) {
|
|
179
|
+
matched += 1;
|
|
180
|
+
} else {
|
|
181
|
+
mismatched += 1;
|
|
182
|
+
if (deltaMax > maxDelta) maxDelta = deltaMax;
|
|
183
|
+
diffs.push({ hour: hourKey, local: localTotals, server: serverTotals, delta });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
summary: {
|
|
191
|
+
days: days.length,
|
|
192
|
+
slots: days.length * 48,
|
|
193
|
+
matched,
|
|
194
|
+
mismatched,
|
|
195
|
+
incomplete,
|
|
196
|
+
maxDelta
|
|
197
|
+
},
|
|
198
|
+
diffs
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { auditOpencodeUsage, buildLocalHourlyTotals };
|
package/src/lib/rollout.js
CHANGED
|
@@ -287,6 +287,8 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
287
287
|
const files = Array.isArray(messageFiles) ? messageFiles : [];
|
|
288
288
|
const totalFiles = files.length;
|
|
289
289
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
290
|
+
const opencodeState = normalizeOpencodeState(cursors?.opencode);
|
|
291
|
+
const messageIndex = opencodeState.messages;
|
|
290
292
|
const touchedBuckets = new Set();
|
|
291
293
|
const defaultSource = normalizeSourceInput(source) || 'opencode';
|
|
292
294
|
|
|
@@ -324,10 +326,14 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
324
326
|
continue;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
const
|
|
329
|
+
const fallbackTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
|
|
330
|
+
const fallbackMessageKey =
|
|
331
|
+
prev && typeof prev.messageKey === 'string' && prev.messageKey.trim() ? prev.messageKey.trim() : null;
|
|
328
332
|
const result = await parseOpencodeMessageFile({
|
|
329
333
|
filePath,
|
|
330
|
-
|
|
334
|
+
messageIndex,
|
|
335
|
+
fallbackTotals,
|
|
336
|
+
fallbackMessageKey,
|
|
331
337
|
hourlyState,
|
|
332
338
|
touchedBuckets,
|
|
333
339
|
source: fileSource
|
|
@@ -338,12 +344,20 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
338
344
|
size,
|
|
339
345
|
mtimeMs,
|
|
340
346
|
lastTotals: result.lastTotals,
|
|
347
|
+
messageKey: result.messageKey || null,
|
|
341
348
|
updatedAt: new Date().toISOString()
|
|
342
349
|
};
|
|
343
350
|
|
|
344
351
|
filesProcessed += 1;
|
|
345
352
|
eventsAggregated += result.eventsAggregated;
|
|
346
353
|
|
|
354
|
+
if (result.messageKey && result.shouldUpdate) {
|
|
355
|
+
messageIndex[result.messageKey] = {
|
|
356
|
+
lastTotals: result.lastTotals,
|
|
357
|
+
updatedAt: new Date().toISOString()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
347
361
|
if (cb) {
|
|
348
362
|
cb({
|
|
349
363
|
index: idx + 1,
|
|
@@ -359,6 +373,8 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
359
373
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
360
374
|
hourlyState.updatedAt = new Date().toISOString();
|
|
361
375
|
cursors.hourly = hourlyState;
|
|
376
|
+
opencodeState.updatedAt = new Date().toISOString();
|
|
377
|
+
cursors.opencode = opencodeState;
|
|
362
378
|
|
|
363
379
|
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
364
380
|
}
|
|
@@ -550,35 +566,86 @@ async function parseGeminiFile({
|
|
|
550
566
|
};
|
|
551
567
|
}
|
|
552
568
|
|
|
553
|
-
async function parseOpencodeMessageFile({
|
|
569
|
+
async function parseOpencodeMessageFile({
|
|
570
|
+
filePath,
|
|
571
|
+
messageIndex,
|
|
572
|
+
fallbackTotals,
|
|
573
|
+
fallbackMessageKey,
|
|
574
|
+
hourlyState,
|
|
575
|
+
touchedBuckets,
|
|
576
|
+
source
|
|
577
|
+
}) {
|
|
578
|
+
const fallbackKey =
|
|
579
|
+
typeof fallbackMessageKey === 'string' && fallbackMessageKey.trim() ? fallbackMessageKey.trim() : null;
|
|
580
|
+
const legacyTotals = fallbackTotals && typeof fallbackTotals === 'object' ? fallbackTotals : null;
|
|
581
|
+
const fallbackEntry = messageIndex && fallbackKey ? messageIndex[fallbackKey] : null;
|
|
582
|
+
const fallbackLastTotals =
|
|
583
|
+
fallbackEntry && typeof fallbackEntry.lastTotals === 'object' ? fallbackEntry.lastTotals : legacyTotals;
|
|
584
|
+
|
|
554
585
|
const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
555
|
-
if (!raw.trim())
|
|
586
|
+
if (!raw.trim()) {
|
|
587
|
+
return {
|
|
588
|
+
messageKey: fallbackKey,
|
|
589
|
+
lastTotals: fallbackLastTotals,
|
|
590
|
+
eventsAggregated: 0,
|
|
591
|
+
shouldUpdate: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
556
594
|
|
|
557
595
|
let msg;
|
|
558
596
|
try {
|
|
559
597
|
msg = JSON.parse(raw);
|
|
560
598
|
} catch (_e) {
|
|
561
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
messageKey: fallbackKey,
|
|
601
|
+
lastTotals: fallbackLastTotals,
|
|
602
|
+
eventsAggregated: 0,
|
|
603
|
+
shouldUpdate: false
|
|
604
|
+
};
|
|
562
605
|
}
|
|
563
606
|
|
|
607
|
+
const messageKey = deriveOpencodeMessageKey(msg, filePath);
|
|
608
|
+
const prev = messageIndex && messageKey ? messageIndex[messageKey] : null;
|
|
609
|
+
const indexTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
|
|
610
|
+
const fallbackMatch = !fallbackKey || fallbackKey === messageKey;
|
|
611
|
+
const lastTotals = indexTotals || (fallbackMatch ? fallbackLastTotals : null);
|
|
612
|
+
|
|
564
613
|
const currentTotals = normalizeOpencodeTokens(msg?.tokens);
|
|
565
|
-
if (!currentTotals)
|
|
614
|
+
if (!currentTotals) {
|
|
615
|
+
return { messageKey, lastTotals, eventsAggregated: 0, shouldUpdate: false };
|
|
616
|
+
}
|
|
566
617
|
|
|
567
618
|
const delta = diffGeminiTotals(currentTotals, lastTotals);
|
|
568
|
-
if (!delta || isAllZeroUsage(delta))
|
|
619
|
+
if (!delta || isAllZeroUsage(delta)) {
|
|
620
|
+
return { messageKey, lastTotals: currentTotals, eventsAggregated: 0, shouldUpdate: true };
|
|
621
|
+
}
|
|
569
622
|
|
|
570
623
|
const timestampMs = coerceEpochMs(msg?.time?.completed) || coerceEpochMs(msg?.time?.created);
|
|
571
|
-
if (!timestampMs)
|
|
624
|
+
if (!timestampMs) {
|
|
625
|
+
return {
|
|
626
|
+
messageKey,
|
|
627
|
+
lastTotals,
|
|
628
|
+
eventsAggregated: 0,
|
|
629
|
+
shouldUpdate: Boolean(lastTotals)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
572
632
|
|
|
573
633
|
const tsIso = new Date(timestampMs).toISOString();
|
|
574
634
|
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
575
|
-
if (!bucketStart)
|
|
635
|
+
if (!bucketStart) {
|
|
636
|
+
return {
|
|
637
|
+
messageKey,
|
|
638
|
+
lastTotals,
|
|
639
|
+
eventsAggregated: 0,
|
|
640
|
+
shouldUpdate: Boolean(lastTotals)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
576
643
|
|
|
577
644
|
const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
|
|
578
645
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
579
646
|
addTotals(bucket.totals, delta);
|
|
580
647
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
581
|
-
return { lastTotals: currentTotals, eventsAggregated: 1 };
|
|
648
|
+
return { messageKey, lastTotals: currentTotals, eventsAggregated: 1, shouldUpdate: true };
|
|
582
649
|
}
|
|
583
650
|
|
|
584
651
|
async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets }) {
|
|
@@ -982,6 +1049,27 @@ function normalizeHourlyState(raw) {
|
|
|
982
1049
|
};
|
|
983
1050
|
}
|
|
984
1051
|
|
|
1052
|
+
function normalizeOpencodeState(raw) {
|
|
1053
|
+
const state = raw && typeof raw === 'object' ? raw : {};
|
|
1054
|
+
const messages = state.messages && typeof state.messages === 'object' ? state.messages : {};
|
|
1055
|
+
return {
|
|
1056
|
+
messages,
|
|
1057
|
+
updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function normalizeMessageKeyPart(value) {
|
|
1062
|
+
if (typeof value !== 'string') return '';
|
|
1063
|
+
return value.trim();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function deriveOpencodeMessageKey(msg, fallback) {
|
|
1067
|
+
const sessionId = normalizeMessageKeyPart(msg?.sessionID || msg?.sessionId || msg?.session_id);
|
|
1068
|
+
const messageId = normalizeMessageKeyPart(msg?.id || msg?.messageID || msg?.messageId);
|
|
1069
|
+
if (sessionId && messageId) return `${sessionId}|${messageId}`;
|
|
1070
|
+
return fallback;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
985
1073
|
function getHourlyBucket(state, source, model, hourStart) {
|
|
986
1074
|
const buckets = state.buckets;
|
|
987
1075
|
const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
@@ -1114,10 +1202,12 @@ function normalizeOpencodeTokens(tokens) {
|
|
|
1114
1202
|
const output = toNonNegativeInt(tokens.output);
|
|
1115
1203
|
const reasoning = toNonNegativeInt(tokens.reasoning);
|
|
1116
1204
|
const cached = toNonNegativeInt(tokens.cache?.read);
|
|
1117
|
-
const
|
|
1205
|
+
const cacheWrite = toNonNegativeInt(tokens.cache?.write);
|
|
1206
|
+
const inputTokens = input + cacheWrite;
|
|
1207
|
+
const total = inputTokens + output + reasoning;
|
|
1118
1208
|
|
|
1119
1209
|
return {
|
|
1120
|
-
input_tokens:
|
|
1210
|
+
input_tokens: inputTokens,
|
|
1121
1211
|
cached_input_tokens: cached,
|
|
1122
1212
|
output_tokens: output,
|
|
1123
1213
|
reasoning_output_tokens: reasoning,
|
|
@@ -1217,7 +1307,7 @@ function normalizeUsage(u) {
|
|
|
1217
1307
|
}
|
|
1218
1308
|
|
|
1219
1309
|
function normalizeClaudeUsage(u) {
|
|
1220
|
-
const inputTokens = toNonNegativeInt(u?.input_tokens);
|
|
1310
|
+
const inputTokens = toNonNegativeInt(u?.input_tokens) + toNonNegativeInt(u?.cache_creation_input_tokens);
|
|
1221
1311
|
const outputTokens = toNonNegativeInt(u?.output_tokens);
|
|
1222
1312
|
const hasTotal = u && Object.prototype.hasOwnProperty.call(u, 'total_tokens');
|
|
1223
1313
|
const totalTokens = hasTotal ? toNonNegativeInt(u?.total_tokens) : inputTokens + outputTokens;
|