sidekick-shared 0.25.0 → 0.26.0
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/CHANGELOG.md +57 -0
- package/README.md +49 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Usage events collected from session logs, cached per session.
|
|
4
|
+
*
|
|
5
|
+
* Feeds the billing-block view and the log-derived daily/weekly/monthly
|
|
6
|
+
* reports, so CLI-only users get numbers without the extension's history
|
|
7
|
+
* store. Each session is read once through its provider reader; the
|
|
8
|
+
* extracted usage is cached under `<configDir>/usage-cache/` keyed by the
|
|
9
|
+
* session's size and mtime, so a repeat run only re-reads sessions that
|
|
10
|
+
* changed. Token counts are cached raw and priced at load time, so a pricing
|
|
11
|
+
* catalog refresh never leaves stale costs behind. Cache files are touched on
|
|
12
|
+
* every hit and pruned least-recently-used, once per collection.
|
|
13
|
+
*
|
|
14
|
+
* Node-only: reads session files and the cache directory.
|
|
15
|
+
*
|
|
16
|
+
* @module usage/usageEvents
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.MAX_USAGE_CACHE_FILES = exports.USAGE_CACHE_VERSION = void 0;
|
|
53
|
+
exports.getUsageCacheDir = getUsageCacheDir;
|
|
54
|
+
exports.pruneUsageCache = pruneUsageCache;
|
|
55
|
+
exports.collectUsageEvents = collectUsageEvents;
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const paths_1 = require("../paths");
|
|
59
|
+
const sessionPreviews_1 = require("../sessionPreviews");
|
|
60
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
61
|
+
const atomic_1 = require("../writers/atomic");
|
|
62
|
+
/** Bump when the cached record shape changes; older files are re-read. */
|
|
63
|
+
exports.USAGE_CACHE_VERSION = 1;
|
|
64
|
+
/**
|
|
65
|
+
* Most-recently-used cache files kept on disk. Sized for a busy month of
|
|
66
|
+
* sessions (files are a few KB each); a collection larger than this still
|
|
67
|
+
* completes, it just re-reads what was evicted. Pruning runs once after a
|
|
68
|
+
* collection, never during one, so a working set is never evicted mid-run.
|
|
69
|
+
*/
|
|
70
|
+
exports.MAX_USAGE_CACHE_FILES = 5000;
|
|
71
|
+
function getUsageCacheDir() {
|
|
72
|
+
return path.join((0, paths_1.getConfigDir)(), 'usage-cache');
|
|
73
|
+
}
|
|
74
|
+
function cacheFilePath(dir, provider, sessionId) {
|
|
75
|
+
const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
76
|
+
return path.join(dir, `${provider}--${safe}.json`);
|
|
77
|
+
}
|
|
78
|
+
function toMs(value, label) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return null;
|
|
81
|
+
const ms = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
82
|
+
if (!Number.isFinite(ms))
|
|
83
|
+
throw new RangeError(`Invalid \`${label}\` value: ${String(value)}`);
|
|
84
|
+
return ms;
|
|
85
|
+
}
|
|
86
|
+
/** Mark a cache file as recently used so LRU pruning keeps it. Best-effort. */
|
|
87
|
+
function touch(filePath) {
|
|
88
|
+
try {
|
|
89
|
+
const now = new Date();
|
|
90
|
+
fs.utimesSync(filePath, now, now);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Read-only cache or vanished file; the next miss rewrites it.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function readCache(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
99
|
+
if (parsed?.version !== exports.USAGE_CACHE_VERSION || !Array.isArray(parsed.events))
|
|
100
|
+
return null;
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
|
|
108
|
+
function pruneUsageCache(keep = exports.MAX_USAGE_CACHE_FILES, dir = getUsageCacheDir()) {
|
|
109
|
+
try {
|
|
110
|
+
const entries = fs
|
|
111
|
+
.readdirSync(dir)
|
|
112
|
+
.filter((name) => name.endsWith('.json'))
|
|
113
|
+
.map((name) => {
|
|
114
|
+
const filePath = path.join(dir, name);
|
|
115
|
+
try {
|
|
116
|
+
return { filePath, mtimeMs: fs.statSync(filePath).mtimeMs };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
.filter((entry) => entry !== null)
|
|
123
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
124
|
+
let removed = 0;
|
|
125
|
+
for (const entry of entries.slice(Math.max(0, keep))) {
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(entry.filePath);
|
|
128
|
+
removed += 1;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Already gone; skip.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return removed;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function cachedEventFromUsage(usage, timestamp, model) {
|
|
141
|
+
const record = {
|
|
142
|
+
t: timestamp,
|
|
143
|
+
m: model,
|
|
144
|
+
i: usage.uncachedInputTokens,
|
|
145
|
+
o: usage.outputTokens,
|
|
146
|
+
cw: usage.cacheWriteTokens,
|
|
147
|
+
cr: usage.cacheReadTokens,
|
|
148
|
+
bo: usage.billableOutputTokens,
|
|
149
|
+
tt: usage.totalTokens,
|
|
150
|
+
};
|
|
151
|
+
if (usage.reportedCostUsd !== undefined)
|
|
152
|
+
record.rc = usage.reportedCostUsd;
|
|
153
|
+
return record;
|
|
154
|
+
}
|
|
155
|
+
/** Price a cached event with the current catalog (or its provider-reported cost). */
|
|
156
|
+
function priceCachedEvent(cached, provider, sessionId, project) {
|
|
157
|
+
const usage = {
|
|
158
|
+
uncachedInputTokens: cached.i,
|
|
159
|
+
cacheReadTokens: cached.cr,
|
|
160
|
+
cacheWriteTokens: cached.cw,
|
|
161
|
+
outputTokens: cached.o,
|
|
162
|
+
reasoningTokens: 0,
|
|
163
|
+
reasoningIncludedInOutput: true,
|
|
164
|
+
cacheInclusiveInputTokens: cached.i + cached.cr + cached.cw,
|
|
165
|
+
billableOutputTokens: cached.bo,
|
|
166
|
+
totalTokens: cached.tt,
|
|
167
|
+
...(cached.rc !== undefined ? { reportedCostUsd: cached.rc } : {}),
|
|
168
|
+
provenance: { semantics: 'sidekick-disjoint' },
|
|
169
|
+
};
|
|
170
|
+
const priced = (0, usageNormalization_1.calculateNormalizedUsageCost)({
|
|
171
|
+
usage,
|
|
172
|
+
modelId: cached.m === 'unknown' ? undefined : cached.m,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
timestamp: cached.t,
|
|
176
|
+
provider,
|
|
177
|
+
sessionId,
|
|
178
|
+
project,
|
|
179
|
+
model: cached.m,
|
|
180
|
+
tokens: {
|
|
181
|
+
inputTokens: cached.i,
|
|
182
|
+
outputTokens: cached.o,
|
|
183
|
+
cacheWriteTokens: cached.cw,
|
|
184
|
+
cacheReadTokens: cached.cr,
|
|
185
|
+
totalTokens: cached.tt,
|
|
186
|
+
},
|
|
187
|
+
costUsd: priced.costUsd,
|
|
188
|
+
costProvenance: priced.source,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function readSessionUsage(provider, preview) {
|
|
192
|
+
const reader = provider.createReader(preview.filePath);
|
|
193
|
+
const events = reader.readAll();
|
|
194
|
+
reader.flush();
|
|
195
|
+
const cached = [];
|
|
196
|
+
for (const event of events) {
|
|
197
|
+
const usage = (0, usageNormalization_1.extractNormalizedUsage)(event);
|
|
198
|
+
if (!usage)
|
|
199
|
+
continue;
|
|
200
|
+
const timestamp = Date.parse(event.timestamp);
|
|
201
|
+
if (!Number.isFinite(timestamp))
|
|
202
|
+
continue;
|
|
203
|
+
cached.push(cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown'));
|
|
204
|
+
}
|
|
205
|
+
return cached;
|
|
206
|
+
}
|
|
207
|
+
function yieldToEventLoop() {
|
|
208
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Collect priced usage events for every session touched in the window.
|
|
212
|
+
*
|
|
213
|
+
* Sessions are enumerated with `listSessionPreviewsAsync` (the `since`
|
|
214
|
+
* cutoff applies to the session mtime there and to the event timestamp
|
|
215
|
+
* here), read once each unless the cache already holds their fingerprint,
|
|
216
|
+
* and yielded between reads so long-lived hosts stay responsive.
|
|
217
|
+
*/
|
|
218
|
+
async function collectUsageEvents(options) {
|
|
219
|
+
const sinceMs = toMs(options.since, 'since');
|
|
220
|
+
const untilMs = toMs(options.until, 'until');
|
|
221
|
+
const cacheDir = options.cacheDir ?? getUsageCacheDir();
|
|
222
|
+
const useCache = !options.noCache;
|
|
223
|
+
const listed = await (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
|
|
224
|
+
since: options.since,
|
|
225
|
+
workspacePath: options.workspacePath,
|
|
226
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
227
|
+
concurrency: options.concurrency,
|
|
228
|
+
});
|
|
229
|
+
const diagnostics = [...listed.diagnostics];
|
|
230
|
+
for (const diagnostic of listed.diagnostics)
|
|
231
|
+
options.onDiagnostic?.(diagnostic);
|
|
232
|
+
const providerById = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
233
|
+
const events = [];
|
|
234
|
+
const sessions = [];
|
|
235
|
+
let cacheHits = 0;
|
|
236
|
+
let cacheMisses = 0;
|
|
237
|
+
let wroteCache = false;
|
|
238
|
+
for (const preview of listed.previews) {
|
|
239
|
+
const provider = providerById.get(preview.provider);
|
|
240
|
+
if (!provider)
|
|
241
|
+
continue;
|
|
242
|
+
const fingerprint = `${preview.sizeBytes}:${preview.modifiedAt}`;
|
|
243
|
+
const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
|
|
244
|
+
let cachedEvents = null;
|
|
245
|
+
let project = preview.workspacePath;
|
|
246
|
+
let fromCache = false;
|
|
247
|
+
if (useCache) {
|
|
248
|
+
const cached = readCache(filePath);
|
|
249
|
+
if (cached && cached.fingerprint === fingerprint && cached.sessionId === preview.sessionId) {
|
|
250
|
+
cachedEvents = cached.events;
|
|
251
|
+
project = project ?? cached.project;
|
|
252
|
+
fromCache = true;
|
|
253
|
+
cacheHits += 1;
|
|
254
|
+
touch(filePath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!cachedEvents) {
|
|
258
|
+
cacheMisses += 1;
|
|
259
|
+
try {
|
|
260
|
+
cachedEvents = readSessionUsage(provider, preview);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const diagnostic = {
|
|
264
|
+
providerId: preview.provider,
|
|
265
|
+
kind: 'read_failed',
|
|
266
|
+
severity: 'warning',
|
|
267
|
+
phase: 'read',
|
|
268
|
+
message: `Could not read usage for ${preview.provider} session ${preview.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
269
|
+
};
|
|
270
|
+
diagnostics.push(diagnostic);
|
|
271
|
+
options.onDiagnostic?.(diagnostic);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (useCache) {
|
|
275
|
+
const record = {
|
|
276
|
+
version: exports.USAGE_CACHE_VERSION,
|
|
277
|
+
provider: preview.provider,
|
|
278
|
+
sessionId: preview.sessionId,
|
|
279
|
+
fingerprint,
|
|
280
|
+
project,
|
|
281
|
+
events: cachedEvents,
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
await (0, atomic_1.atomicWriteJson)(filePath, record);
|
|
285
|
+
wroteCache = true;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// The cache is an accelerator; a failed write only costs a re-read.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
await yieldToEventLoop();
|
|
292
|
+
}
|
|
293
|
+
let first = null;
|
|
294
|
+
let last = null;
|
|
295
|
+
let count = 0;
|
|
296
|
+
for (const cached of cachedEvents) {
|
|
297
|
+
if (sinceMs !== null && cached.t < sinceMs)
|
|
298
|
+
continue;
|
|
299
|
+
if (untilMs !== null && cached.t > untilMs)
|
|
300
|
+
continue;
|
|
301
|
+
events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project));
|
|
302
|
+
first = first === null ? cached.t : Math.min(first, cached.t);
|
|
303
|
+
last = last === null ? cached.t : Math.max(last, cached.t);
|
|
304
|
+
count += 1;
|
|
305
|
+
}
|
|
306
|
+
sessions.push({
|
|
307
|
+
provider: preview.provider,
|
|
308
|
+
sessionId: preview.sessionId,
|
|
309
|
+
filePath: preview.filePath,
|
|
310
|
+
project,
|
|
311
|
+
fingerprint,
|
|
312
|
+
firstTimestamp: first,
|
|
313
|
+
lastTimestamp: last,
|
|
314
|
+
eventCount: count,
|
|
315
|
+
fromCache,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (wroteCache)
|
|
319
|
+
pruneUsageCache(exports.MAX_USAGE_CACHE_FILES, cacheDir);
|
|
320
|
+
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
321
|
+
return { events, sessions, diagnostics, cacheHits, cacheMisses };
|
|
322
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily, weekly, monthly, and per-session usage rows bucketed from usage
|
|
3
|
+
* events by the time of each event, so a session that crosses midnight is
|
|
4
|
+
* split across the days it actually ran in. Browser-safe: no Node imports.
|
|
5
|
+
*
|
|
6
|
+
* @module usage/usageReports
|
|
7
|
+
*/
|
|
8
|
+
import type { AggregatedCostProvenance } from '../aggregation/types';
|
|
9
|
+
import type { ProviderId } from '../providers/types';
|
|
10
|
+
import type { TokenTotals } from '../types/historicalData';
|
|
11
|
+
import type { UsageEventRecord } from './usageEvents';
|
|
12
|
+
export type UsageGranularity = 'day' | 'week' | 'month' | 'session';
|
|
13
|
+
/** Extra dimensions a bucket is split by, beyond the time key. */
|
|
14
|
+
export type UsageGroupDimension = 'provider' | 'project' | 'model';
|
|
15
|
+
export interface BucketUsageOptions {
|
|
16
|
+
granularity: UsageGranularity;
|
|
17
|
+
/** Key by the UTC calendar instead of the local calendar (default false). */
|
|
18
|
+
utc?: boolean;
|
|
19
|
+
/** First day of the week for `week` keys: 0 = Sunday, 1 = Monday (default). */
|
|
20
|
+
weekStartsOn?: 0 | 1;
|
|
21
|
+
/** Dimensions to group by in addition to the time key (default `['provider']`). */
|
|
22
|
+
groupBy?: readonly UsageGroupDimension[];
|
|
23
|
+
}
|
|
24
|
+
export interface UsageBucketRow {
|
|
25
|
+
/** `YYYY-MM-DD` for days and week starts, `YYYY-MM` for months, the session id for sessions. */
|
|
26
|
+
key: string;
|
|
27
|
+
granularity: UsageGranularity;
|
|
28
|
+
provider: ProviderId | null;
|
|
29
|
+
project: string | null;
|
|
30
|
+
/** Set when grouping by model; null for rows spanning every model. */
|
|
31
|
+
model: string | null;
|
|
32
|
+
/** Session id for `session` rows; null otherwise. */
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
/** Models seen in the row, most tokens first. */
|
|
35
|
+
models: string[];
|
|
36
|
+
calls: number;
|
|
37
|
+
/** Distinct sessions contributing to the row. */
|
|
38
|
+
sessions: number;
|
|
39
|
+
tokens: TokenTotals;
|
|
40
|
+
/** Provider-semantics-aware total (`summarizeTokens().total`). */
|
|
41
|
+
totalTokens: number;
|
|
42
|
+
/** Priced portion of the cost; read `costProvenance` before labelling it. */
|
|
43
|
+
costUsd: number;
|
|
44
|
+
costProvenance: AggregatedCostProvenance;
|
|
45
|
+
unpricedCalls: number;
|
|
46
|
+
/** First and last event in the row, ms since epoch. */
|
|
47
|
+
firstTimestamp: number;
|
|
48
|
+
lastTimestamp: number;
|
|
49
|
+
}
|
|
50
|
+
export interface UsageTotals {
|
|
51
|
+
calls: number;
|
|
52
|
+
sessions: number;
|
|
53
|
+
tokens: TokenTotals;
|
|
54
|
+
totalTokens: number;
|
|
55
|
+
costUsd: number;
|
|
56
|
+
costProvenance: AggregatedCostProvenance;
|
|
57
|
+
unpricedCalls: number;
|
|
58
|
+
}
|
|
59
|
+
/** The `YYYY-MM-DD` key of the week containing `ms`, starting on `weekStartsOn`. */
|
|
60
|
+
export declare function weekKey(ms: number, options?: {
|
|
61
|
+
utc?: boolean;
|
|
62
|
+
weekStartsOn?: 0 | 1;
|
|
63
|
+
}): string;
|
|
64
|
+
/** The time key for one event at the requested granularity. */
|
|
65
|
+
export declare function usageBucketKey(event: Pick<UsageEventRecord, 'timestamp' | 'sessionId'>, options: BucketUsageOptions): string;
|
|
66
|
+
/**
|
|
67
|
+
* Group usage events into rows. Rows are sorted by key (sessions by first
|
|
68
|
+
* event), then provider, project, and model, so tables read oldest first.
|
|
69
|
+
*/
|
|
70
|
+
export declare function bucketUsage(events: readonly UsageEventRecord[], options: BucketUsageOptions): UsageBucketRow[];
|
|
71
|
+
/**
|
|
72
|
+
* Sum rows that do not overlap (for example every row of one `bucketUsage`
|
|
73
|
+
* call). Pass the events too when rows are not per-session, so distinct
|
|
74
|
+
* sessions are counted once instead of once per row they appear in.
|
|
75
|
+
*/
|
|
76
|
+
export declare function summarizeUsageRows(rows: readonly UsageBucketRow[], events?: readonly UsageEventRecord[]): UsageTotals;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Daily, weekly, monthly, and per-session usage rows bucketed from usage
|
|
4
|
+
* events by the time of each event, so a session that crosses midnight is
|
|
5
|
+
* split across the days it actually ran in. Browser-safe: no Node imports.
|
|
6
|
+
*
|
|
7
|
+
* @module usage/usageReports
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.weekKey = weekKey;
|
|
11
|
+
exports.usageBucketKey = usageBucketKey;
|
|
12
|
+
exports.bucketUsage = bucketUsage;
|
|
13
|
+
exports.summarizeUsageRows = summarizeUsageRows;
|
|
14
|
+
const costProvenance_1 = require("../aggregation/costProvenance");
|
|
15
|
+
const formatting_1 = require("../formatting");
|
|
16
|
+
function dayParts(ms, utc) {
|
|
17
|
+
const date = new Date(ms);
|
|
18
|
+
return utc
|
|
19
|
+
? {
|
|
20
|
+
year: date.getUTCFullYear(),
|
|
21
|
+
month: date.getUTCMonth(),
|
|
22
|
+
day: date.getUTCDate(),
|
|
23
|
+
weekday: date.getUTCDay(),
|
|
24
|
+
}
|
|
25
|
+
: {
|
|
26
|
+
year: date.getFullYear(),
|
|
27
|
+
month: date.getMonth(),
|
|
28
|
+
day: date.getDate(),
|
|
29
|
+
weekday: date.getDay(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function pad(value) {
|
|
33
|
+
return String(value).padStart(2, '0');
|
|
34
|
+
}
|
|
35
|
+
/** The `YYYY-MM-DD` key of the week containing `ms`, starting on `weekStartsOn`. */
|
|
36
|
+
function weekKey(ms, options = {}) {
|
|
37
|
+
const utc = options.utc ?? false;
|
|
38
|
+
const start = options.weekStartsOn ?? 1;
|
|
39
|
+
const parts = dayParts(ms, utc);
|
|
40
|
+
const offset = (parts.weekday - start + 7) % 7;
|
|
41
|
+
if (utc) {
|
|
42
|
+
return new Date(Date.UTC(parts.year, parts.month, parts.day - offset))
|
|
43
|
+
.toISOString()
|
|
44
|
+
.slice(0, 10);
|
|
45
|
+
}
|
|
46
|
+
return (0, formatting_1.formatLocalDateKey)(new Date(parts.year, parts.month, parts.day - offset));
|
|
47
|
+
}
|
|
48
|
+
/** The time key for one event at the requested granularity. */
|
|
49
|
+
function usageBucketKey(event, options) {
|
|
50
|
+
const utc = options.utc ?? false;
|
|
51
|
+
switch (options.granularity) {
|
|
52
|
+
case 'session':
|
|
53
|
+
return event.sessionId;
|
|
54
|
+
case 'week':
|
|
55
|
+
return weekKey(event.timestamp, { utc, weekStartsOn: options.weekStartsOn });
|
|
56
|
+
case 'month': {
|
|
57
|
+
const parts = dayParts(event.timestamp, utc);
|
|
58
|
+
return `${parts.year}-${pad(parts.month + 1)}`;
|
|
59
|
+
}
|
|
60
|
+
case 'day':
|
|
61
|
+
default:
|
|
62
|
+
return utc
|
|
63
|
+
? new Date(event.timestamp).toISOString().slice(0, 10)
|
|
64
|
+
: (0, formatting_1.formatLocalDateKey)(event.timestamp);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function emptyTokens() {
|
|
68
|
+
return { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
|
|
69
|
+
}
|
|
70
|
+
function addTokens(target, source) {
|
|
71
|
+
target.inputTokens += source.inputTokens;
|
|
72
|
+
target.outputTokens += source.outputTokens;
|
|
73
|
+
target.cacheWriteTokens += source.cacheWriteTokens;
|
|
74
|
+
target.cacheReadTokens += source.cacheReadTokens;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Group usage events into rows. Rows are sorted by key (sessions by first
|
|
78
|
+
* event), then provider, project, and model, so tables read oldest first.
|
|
79
|
+
*/
|
|
80
|
+
function bucketUsage(events, options) {
|
|
81
|
+
const groupBy = new Set(options.groupBy ?? ['provider']);
|
|
82
|
+
const isSession = options.granularity === 'session';
|
|
83
|
+
const byProvider = isSession || groupBy.has('provider');
|
|
84
|
+
const byProject = isSession || groupBy.has('project');
|
|
85
|
+
const byModel = groupBy.has('model');
|
|
86
|
+
const buckets = new Map();
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
const key = usageBucketKey(event, options);
|
|
89
|
+
const provider = byProvider ? event.provider : null;
|
|
90
|
+
const project = byProject ? event.project : null;
|
|
91
|
+
const model = byModel ? event.model : null;
|
|
92
|
+
const id = [key, provider ?? '', project ?? '', model ?? ''].join('|');
|
|
93
|
+
let acc = buckets.get(id);
|
|
94
|
+
if (!acc) {
|
|
95
|
+
acc = {
|
|
96
|
+
row: {
|
|
97
|
+
key,
|
|
98
|
+
granularity: options.granularity,
|
|
99
|
+
provider,
|
|
100
|
+
project,
|
|
101
|
+
model,
|
|
102
|
+
sessionId: isSession ? event.sessionId : null,
|
|
103
|
+
models: [],
|
|
104
|
+
calls: 0,
|
|
105
|
+
sessions: 0,
|
|
106
|
+
tokens: emptyTokens(),
|
|
107
|
+
totalTokens: 0,
|
|
108
|
+
costUsd: 0,
|
|
109
|
+
costProvenance: 'none',
|
|
110
|
+
unpricedCalls: 0,
|
|
111
|
+
firstTimestamp: event.timestamp,
|
|
112
|
+
lastTimestamp: event.timestamp,
|
|
113
|
+
},
|
|
114
|
+
sessionIds: new Set(),
|
|
115
|
+
modelTokens: new Map(),
|
|
116
|
+
reportedCalls: 0,
|
|
117
|
+
estimatedCalls: 0,
|
|
118
|
+
};
|
|
119
|
+
buckets.set(id, acc);
|
|
120
|
+
}
|
|
121
|
+
const { row } = acc;
|
|
122
|
+
row.calls += 1;
|
|
123
|
+
addTokens(row.tokens, event.tokens);
|
|
124
|
+
row.totalTokens += event.tokens.totalTokens;
|
|
125
|
+
row.firstTimestamp = Math.min(row.firstTimestamp, event.timestamp);
|
|
126
|
+
row.lastTimestamp = Math.max(row.lastTimestamp, event.timestamp);
|
|
127
|
+
acc.sessionIds.add(event.sessionId);
|
|
128
|
+
acc.modelTokens.set(event.model, (acc.modelTokens.get(event.model) ?? 0) + event.tokens.totalTokens);
|
|
129
|
+
if (event.costUsd !== null) {
|
|
130
|
+
row.costUsd += event.costUsd;
|
|
131
|
+
if (event.costProvenance === 'provider-reported')
|
|
132
|
+
acc.reportedCalls += 1;
|
|
133
|
+
else
|
|
134
|
+
acc.estimatedCalls += 1;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
row.unpricedCalls += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const rows = [];
|
|
141
|
+
for (const acc of buckets.values()) {
|
|
142
|
+
acc.row.sessions = acc.sessionIds.size;
|
|
143
|
+
acc.row.models = [...acc.modelTokens.entries()]
|
|
144
|
+
.sort((a, b) => b[1] - a[1])
|
|
145
|
+
.map(([model]) => model);
|
|
146
|
+
acc.row.costProvenance = (0, costProvenance_1.classifyCostProvenance)({
|
|
147
|
+
reportedCalls: acc.reportedCalls,
|
|
148
|
+
estimatedCalls: acc.estimatedCalls,
|
|
149
|
+
unpricedCalls: acc.row.unpricedCalls,
|
|
150
|
+
});
|
|
151
|
+
rows.push(acc.row);
|
|
152
|
+
}
|
|
153
|
+
return rows.sort((a, b) => (isSession ? a.firstTimestamp - b.firstTimestamp : a.key.localeCompare(b.key)) ||
|
|
154
|
+
(a.provider ?? '').localeCompare(b.provider ?? '') ||
|
|
155
|
+
(a.project ?? '').localeCompare(b.project ?? '') ||
|
|
156
|
+
(a.model ?? '').localeCompare(b.model ?? ''));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sum rows that do not overlap (for example every row of one `bucketUsage`
|
|
160
|
+
* call). Pass the events too when rows are not per-session, so distinct
|
|
161
|
+
* sessions are counted once instead of once per row they appear in.
|
|
162
|
+
*/
|
|
163
|
+
function summarizeUsageRows(rows, events) {
|
|
164
|
+
const totals = {
|
|
165
|
+
calls: 0,
|
|
166
|
+
sessions: 0,
|
|
167
|
+
tokens: emptyTokens(),
|
|
168
|
+
totalTokens: 0,
|
|
169
|
+
costUsd: 0,
|
|
170
|
+
costProvenance: 'none',
|
|
171
|
+
unpricedCalls: 0,
|
|
172
|
+
};
|
|
173
|
+
let reportedCalls = 0;
|
|
174
|
+
let estimatedCalls = 0;
|
|
175
|
+
const sessionIds = new Set();
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
totals.calls += row.calls;
|
|
178
|
+
addTokens(totals.tokens, row.tokens);
|
|
179
|
+
totals.totalTokens += row.totalTokens;
|
|
180
|
+
totals.costUsd += row.costUsd;
|
|
181
|
+
totals.unpricedCalls += row.unpricedCalls;
|
|
182
|
+
const priced = row.calls - row.unpricedCalls;
|
|
183
|
+
if (row.costProvenance === 'reported')
|
|
184
|
+
reportedCalls += priced;
|
|
185
|
+
else if (row.costProvenance === 'estimated')
|
|
186
|
+
estimatedCalls += priced;
|
|
187
|
+
else if (row.costProvenance === 'mixed') {
|
|
188
|
+
reportedCalls += 1;
|
|
189
|
+
estimatedCalls += 1;
|
|
190
|
+
}
|
|
191
|
+
if (row.sessionId)
|
|
192
|
+
sessionIds.add(row.sessionId);
|
|
193
|
+
}
|
|
194
|
+
if (events) {
|
|
195
|
+
for (const event of events)
|
|
196
|
+
sessionIds.add(event.sessionId);
|
|
197
|
+
totals.sessions = sessionIds.size;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
totals.sessions = sessionIds.size || rows.reduce((sum, row) => sum + row.sessions, 0);
|
|
201
|
+
}
|
|
202
|
+
totals.costProvenance = (0, costProvenance_1.classifyCostProvenance)({
|
|
203
|
+
reportedCalls,
|
|
204
|
+
estimatedCalls,
|
|
205
|
+
unpricedCalls: totals.unpricedCalls,
|
|
206
|
+
});
|
|
207
|
+
return totals;
|
|
208
|
+
}
|
|
@@ -9,6 +9,12 @@ export interface CreateWatcherOptions {
|
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
callbacks: SessionWatcherCallbacks;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the session a command should act on: an exact or unique-prefix
|
|
14
|
+
* match of `sessionId` among the workspace's sessions, or the most recent
|
|
15
|
+
* session when no id is given. Throws with the available ids on a miss.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveSessionPath(provider: SessionProviderBase, workspacePath: string, sessionId?: string): string;
|
|
12
18
|
export declare function createWatcher(options: CreateWatcherOptions): {
|
|
13
19
|
watcher: SessionWatcher;
|
|
14
20
|
sessionPath: string;
|
package/dist/watchers/factory.js
CHANGED
|
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.resolveSessionPath = resolveSessionPath;
|
|
39
40
|
exports.createWatcher = createWatcher;
|
|
40
41
|
const os = __importStar(require("os"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
@@ -48,37 +49,41 @@ function getOpenCodeDataDir() {
|
|
|
48
49
|
return path.join(xdg, 'opencode');
|
|
49
50
|
return path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the session a command should act on: an exact or unique-prefix
|
|
54
|
+
* match of `sessionId` among the workspace's sessions, or the most recent
|
|
55
|
+
* session when no id is given. Throws with the available ids on a miss.
|
|
56
|
+
*/
|
|
57
|
+
function resolveSessionPath(provider, workspacePath, sessionId) {
|
|
53
58
|
const sessions = provider.findAllSessions(workspacePath);
|
|
54
59
|
if (sessions.length === 0) {
|
|
55
60
|
throw new Error(`No sessions found for ${provider.displayName} in ${workspacePath}`);
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
if (candidates.length > 1) {
|
|
72
|
-
throw new Error(`Session ${sessionId} is ambiguous. Matches: ${candidates
|
|
73
|
-
.slice(0, 5)
|
|
74
|
-
.map((candidate) => candidate.id)
|
|
75
|
-
.join(', ')}`);
|
|
76
|
-
}
|
|
77
|
-
sessionPath = candidates[0].sessionPath;
|
|
62
|
+
if (!sessionId)
|
|
63
|
+
return sessions[0]; // most recent
|
|
64
|
+
const matches = sessions.map((sessionPath) => ({
|
|
65
|
+
sessionPath,
|
|
66
|
+
id: provider.getSessionId(sessionPath),
|
|
67
|
+
}));
|
|
68
|
+
const exact = matches.filter((candidate) => candidate.id === sessionId);
|
|
69
|
+
const candidates = exact.length > 0 ? exact : matches.filter((candidate) => candidate.id.startsWith(sessionId));
|
|
70
|
+
if (candidates.length === 0) {
|
|
71
|
+
throw new Error(`Session ${sessionId} not found. Available: ${sessions
|
|
72
|
+
.slice(0, 5)
|
|
73
|
+
.map((s) => path.basename(s))
|
|
74
|
+
.join(', ')}`);
|
|
78
75
|
}
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
if (candidates.length > 1) {
|
|
77
|
+
throw new Error(`Session ${sessionId} is ambiguous. Matches: ${candidates
|
|
78
|
+
.slice(0, 5)
|
|
79
|
+
.map((candidate) => candidate.id)
|
|
80
|
+
.join(', ')}`);
|
|
81
81
|
}
|
|
82
|
+
return candidates[0].sessionPath;
|
|
83
|
+
}
|
|
84
|
+
function createWatcher(options) {
|
|
85
|
+
const { provider, workspacePath, sessionId, callbacks } = options;
|
|
86
|
+
const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
|
|
82
87
|
const watcher = createWatcherForProvider(provider, sessionPath, callbacks);
|
|
83
88
|
return { watcher, sessionPath };
|
|
84
89
|
}
|