sidekick-shared 0.24.5 → 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 +92 -0
- package/README.md +205 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +170 -0
- package/dist/accountRegistry.d.ts +25 -2
- package/dist/accountRegistry.js +41 -13
- 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 +19 -4
- package/dist/browser.js +43 -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/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- 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 +67 -23
- package/dist/index.js +138 -26
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +23 -4
- package/dist/node.js +50 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +72 -6
- package/dist/observedSessionCollector.js +440 -47
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +28 -0
- package/dist/parsers/sessionActivityDetector.js +110 -15
- 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/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +211 -131
- package/dist/providers/codex.d.ts +20 -1
- package/dist/providers/codex.js +308 -244
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +20 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +22 -1
- package/dist/providers/openCode.js +460 -126
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +82 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +34 -4
- package/dist/quota.js +37 -9
- package/dist/quotaHistory.d.ts +3 -2
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- 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/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +16 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +141 -2
- package/dist/schemas/quota.js +20 -8
- package/dist/schemas/quotaHistory.d.ts +3 -2
- 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/tokenEstimation.d.ts +1 -1
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +33 -2
- package/dist/sessionPreviews.js +333 -6
- 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 +8 -2
- package/dist/types/observedSessionV1.js +88 -25
- 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 +22 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Import finished sessions from every provider into the history store.
|
|
4
|
+
*
|
|
5
|
+
* The VS Code extension used to import Claude Code only, by hand-parsing
|
|
6
|
+
* JSONL files; the CLI could not import at all. This walks the sessions
|
|
7
|
+
* every provider knows about, reads each once through the unified stats
|
|
8
|
+
* path, and hands a `SessionSummary` to the caller, which applies it to the
|
|
9
|
+
* store it owns (the extension's debounced persistence service or the CLI's
|
|
10
|
+
* locked atomic writer). Dedupe is the caller's `isImported` answer
|
|
11
|
+
* (imported file paths plus persisted session ids); the capped per-session
|
|
12
|
+
* list is deliberately not used as the dedupe key.
|
|
13
|
+
*
|
|
14
|
+
* Node-only: reads session files.
|
|
15
|
+
*
|
|
16
|
+
* @module usage/importSessionHistory
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS = void 0;
|
|
20
|
+
exports.importSessionHistory = importSessionHistory;
|
|
21
|
+
const historicalStore_1 = require("../historicalStore");
|
|
22
|
+
const sessionPreviews_1 = require("../sessionPreviews");
|
|
23
|
+
const sessionStats_1 = require("../sessionStats");
|
|
24
|
+
/** Files modified more recently than this are skipped: they may still be live. */
|
|
25
|
+
exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS = 60_000;
|
|
26
|
+
function yieldToEventLoop() {
|
|
27
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
28
|
+
}
|
|
29
|
+
async function importSessionHistory(options) {
|
|
30
|
+
const now = options.now ?? new Date();
|
|
31
|
+
const threshold = options.activeFileThresholdMs ?? exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS;
|
|
32
|
+
const listed = await (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
|
|
33
|
+
since: options.since,
|
|
34
|
+
workspacePath: options.workspacePath,
|
|
35
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
36
|
+
});
|
|
37
|
+
const providerById = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
38
|
+
const result = {
|
|
39
|
+
filesFound: listed.previews.length,
|
|
40
|
+
filesProcessed: 0,
|
|
41
|
+
filesSkipped: 0,
|
|
42
|
+
filesUnavailable: 0,
|
|
43
|
+
sessionsImported: 0,
|
|
44
|
+
messagesImported: 0,
|
|
45
|
+
diagnostics: [...listed.diagnostics],
|
|
46
|
+
};
|
|
47
|
+
const total = listed.previews.length;
|
|
48
|
+
let loaded = 0;
|
|
49
|
+
for (const preview of listed.previews) {
|
|
50
|
+
loaded += 1;
|
|
51
|
+
const provider = providerById.get(preview.provider);
|
|
52
|
+
const modifiedMs = Date.parse(preview.modifiedAt);
|
|
53
|
+
const recentlyModified = Number.isFinite(modifiedMs) && now.getTime() - modifiedMs < threshold;
|
|
54
|
+
if (!provider || recentlyModified || options.isImported(preview.sessionId, preview.filePath)) {
|
|
55
|
+
result.filesSkipped += 1;
|
|
56
|
+
options.onProgress?.(loaded, total);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const stats = (0, sessionStats_1.readSessionFileStats)(provider, preview.filePath);
|
|
60
|
+
result.filesProcessed += 1;
|
|
61
|
+
if (stats.availability === 'unavailable') {
|
|
62
|
+
result.filesUnavailable += 1;
|
|
63
|
+
result.diagnostics.push({
|
|
64
|
+
providerId: preview.provider,
|
|
65
|
+
kind: 'read_failed',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
phase: 'read',
|
|
68
|
+
message: `${preview.provider} session ${preview.sessionId}: ${stats.unavailableReason ?? 'unavailable'}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (Object.keys(stats.modelUsage).length === 0) {
|
|
72
|
+
// No usage record at all (for example a prompt that never got an
|
|
73
|
+
// answer): nothing to credit, but mark the file so it is not re-read
|
|
74
|
+
// on every run.
|
|
75
|
+
result.filesUnavailable += 1;
|
|
76
|
+
options.markImported(preview.filePath);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
options.applySummary((0, historicalStore_1.sessionSummaryFromStats)(stats, { project: preview.workspacePath }), preview.filePath);
|
|
80
|
+
options.markImported(preview.filePath);
|
|
81
|
+
result.sessionsImported += 1;
|
|
82
|
+
result.messagesImported += stats.messageCount;
|
|
83
|
+
}
|
|
84
|
+
options.onProgress?.(loaded, total);
|
|
85
|
+
await yieldToEventLoop();
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage events collected from session logs, cached per session.
|
|
3
|
+
*
|
|
4
|
+
* Feeds the billing-block view and the log-derived daily/weekly/monthly
|
|
5
|
+
* reports, so CLI-only users get numbers without the extension's history
|
|
6
|
+
* store. Each session is read once through its provider reader; the
|
|
7
|
+
* extracted usage is cached under `<configDir>/usage-cache/` keyed by the
|
|
8
|
+
* session's size and mtime, so a repeat run only re-reads sessions that
|
|
9
|
+
* changed. Token counts are cached raw and priced at load time, so a pricing
|
|
10
|
+
* catalog refresh never leaves stale costs behind. Cache files are touched on
|
|
11
|
+
* every hit and pruned least-recently-used, once per collection.
|
|
12
|
+
*
|
|
13
|
+
* Node-only: reads session files and the cache directory.
|
|
14
|
+
*
|
|
15
|
+
* @module usage/usageEvents
|
|
16
|
+
*/
|
|
17
|
+
import type { ProviderId, SessionProviderBase, SessionProviderDiagnostic } from '../providers/types';
|
|
18
|
+
import type { TokenTotals } from '../types/historicalData';
|
|
19
|
+
import type { PricingProvenance } from '../usageNormalization';
|
|
20
|
+
/** Bump when the cached record shape changes; older files are re-read. */
|
|
21
|
+
export declare const USAGE_CACHE_VERSION = 1;
|
|
22
|
+
/**
|
|
23
|
+
* Most-recently-used cache files kept on disk. Sized for a busy month of
|
|
24
|
+
* sessions (files are a few KB each); a collection larger than this still
|
|
25
|
+
* completes, it just re-reads what was evicted. Pruning runs once after a
|
|
26
|
+
* collection, never during one, so a working set is never evicted mid-run.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MAX_USAGE_CACHE_FILES = 5000;
|
|
29
|
+
/** One priced usage event from a session log. */
|
|
30
|
+
export interface UsageEventRecord {
|
|
31
|
+
/** Event time in ms since epoch. */
|
|
32
|
+
timestamp: number;
|
|
33
|
+
provider: ProviderId;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
/** Workspace path recorded in the session, when known. */
|
|
36
|
+
project: string | null;
|
|
37
|
+
model: string;
|
|
38
|
+
/** Four buckets plus the provider-semantics-aware total. */
|
|
39
|
+
tokens: TokenTotals & {
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
};
|
|
42
|
+
costUsd: number | null;
|
|
43
|
+
costProvenance: PricingProvenance;
|
|
44
|
+
}
|
|
45
|
+
/** One session that contributed events. */
|
|
46
|
+
export interface UsageSessionRecord {
|
|
47
|
+
provider: ProviderId;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
filePath: string;
|
|
50
|
+
project: string | null;
|
|
51
|
+
fingerprint: string;
|
|
52
|
+
firstTimestamp: number | null;
|
|
53
|
+
lastTimestamp: number | null;
|
|
54
|
+
eventCount: number;
|
|
55
|
+
fromCache: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface CollectUsageEventsOptions {
|
|
58
|
+
providers: SessionProviderBase[];
|
|
59
|
+
/** Only sessions modified after this instant, and only events at or after it. */
|
|
60
|
+
since?: Date | string;
|
|
61
|
+
/** Only events at or before this instant. */
|
|
62
|
+
until?: Date | string;
|
|
63
|
+
/** Restrict to one workspace's sessions. */
|
|
64
|
+
workspacePath?: string;
|
|
65
|
+
/** Preview-read concurrency (see `listSessionPreviewsAsync`). */
|
|
66
|
+
concurrency?: number;
|
|
67
|
+
/** Cache directory (default `<configDir>/usage-cache`). */
|
|
68
|
+
cacheDir?: string;
|
|
69
|
+
/** Skip the on-disk cache entirely. */
|
|
70
|
+
noCache?: boolean;
|
|
71
|
+
onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
|
|
72
|
+
}
|
|
73
|
+
export interface CollectUsageEventsResult {
|
|
74
|
+
/** Events within the window, oldest first. */
|
|
75
|
+
events: UsageEventRecord[];
|
|
76
|
+
sessions: UsageSessionRecord[];
|
|
77
|
+
diagnostics: SessionProviderDiagnostic[];
|
|
78
|
+
cacheHits: number;
|
|
79
|
+
cacheMisses: number;
|
|
80
|
+
}
|
|
81
|
+
export declare function getUsageCacheDir(): string;
|
|
82
|
+
/** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
|
|
83
|
+
export declare function pruneUsageCache(keep?: number, dir?: string): number;
|
|
84
|
+
/**
|
|
85
|
+
* Collect priced usage events for every session touched in the window.
|
|
86
|
+
*
|
|
87
|
+
* Sessions are enumerated with `listSessionPreviewsAsync` (the `since`
|
|
88
|
+
* cutoff applies to the session mtime there and to the event timestamp
|
|
89
|
+
* here), read once each unless the cache already holds their fingerprint,
|
|
90
|
+
* and yielded between reads so long-lived hosts stay responsive.
|
|
91
|
+
*/
|
|
92
|
+
export declare function collectUsageEvents(options: CollectUsageEventsOptions): Promise<CollectUsageEventsResult>;
|
|
@@ -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;
|