copilot-usage-studio 0.1.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/bin/copilot-usage-studio.mjs +10 -0
- package/data/github-copilot-pricing.json +227 -0
- package/dist/copilot-usage-studio/browser/chunk-C6VWIY5S.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-DLWQO3VR.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-F6TIG2GE.js +4 -0
- package/dist/copilot-usage-studio/browser/chunk-JIP7ONRZ.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-RNKEPBEU.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-Z3XIAKMM.js +1 -0
- package/dist/copilot-usage-studio/browser/index.html +13 -0
- package/dist/copilot-usage-studio/browser/main-C6XOJRSH.js +2 -0
- package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +1 -0
- package/dist/copilot-usage-studio/browser/usage-studio.svg +12 -0
- package/docs/local-deployment.md +202 -0
- package/docs/pricing.md +274 -0
- package/docs/scanner-api.md +109 -0
- package/lib/cli.mjs +173 -0
- package/lib/local-runtime.mjs +231 -0
- package/lib/scanner-api.mjs +12 -0
- package/package.json +86 -0
- package/scripts/pricing-utils.mjs +71 -0
- package/scripts/scan-vscode-sessions.mjs +1632 -0
|
@@ -0,0 +1,1632 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir, platform } from 'node:os';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
costBreakdownUsdForTokens,
|
|
7
|
+
costUsdForTokens,
|
|
8
|
+
modelKey,
|
|
9
|
+
normalizeModel,
|
|
10
|
+
pricingModelForModel,
|
|
11
|
+
} from './pricing-utils.mjs';
|
|
12
|
+
|
|
13
|
+
const sessionDataSchemaVersion = 1;
|
|
14
|
+
const pricingData = JSON.parse(
|
|
15
|
+
readFileSync(new URL('../data/github-copilot-pricing.json', import.meta.url), 'utf8'),
|
|
16
|
+
);
|
|
17
|
+
const pricingVersion = pricingData.version;
|
|
18
|
+
const pricingSourceUrl = pricingData.sourceUrl;
|
|
19
|
+
const fallbackPricingModel = pricingData.fallbackModel;
|
|
20
|
+
const traceEventLimit = 1000;
|
|
21
|
+
|
|
22
|
+
const pricing = pricingData.models;
|
|
23
|
+
|
|
24
|
+
let usdToEur = 1;
|
|
25
|
+
let diagnostics = createDiagnostics();
|
|
26
|
+
let DatabaseSync = null;
|
|
27
|
+
let scanInProgress = false;
|
|
28
|
+
|
|
29
|
+
function createDiagnostics() {
|
|
30
|
+
return {
|
|
31
|
+
scannedRoots: [],
|
|
32
|
+
scannedWorkspaces: 0,
|
|
33
|
+
scannedStateDbs: 0,
|
|
34
|
+
enrichedFromStateDbs: 0,
|
|
35
|
+
importedDebugLogSessions: 0,
|
|
36
|
+
importedChatSnapshotSessions: 0,
|
|
37
|
+
debugLogSessionsWithTranscripts: 0,
|
|
38
|
+
transcriptEventsAvailable: 0,
|
|
39
|
+
skippedEmptyDebugLogs: 0,
|
|
40
|
+
skippedChatSnapshotsWithoutRequests: 0,
|
|
41
|
+
skippedDuplicateChatSnapshots: 0,
|
|
42
|
+
warnings: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function defaultCodeUserDirs() {
|
|
47
|
+
const home = homedir();
|
|
48
|
+
|
|
49
|
+
if (platform() === 'win32') {
|
|
50
|
+
const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
51
|
+
return [join(appData, 'Code', 'User'), join(appData, 'Code - Insiders', 'User')];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (platform() === 'darwin') {
|
|
55
|
+
return [
|
|
56
|
+
join(home, 'Library', 'Application Support', 'Code', 'User'),
|
|
57
|
+
join(home, 'Library', 'Application Support', 'Code - Insiders', 'User'),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [join(home, '.config', 'Code', 'User'), join(home, '.config', 'Code - Insiders', 'User')];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safeJson(text) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(text);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadSqliteSupport() {
|
|
73
|
+
const originalEmitWarning = process.emitWarning;
|
|
74
|
+
process.emitWarning = (warning, ...args) => {
|
|
75
|
+
if (String(warning).includes('SQLite is an experimental feature')) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
originalEmitWarning.call(process, warning, ...args);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const sqlite = await import('node:sqlite');
|
|
83
|
+
return sqlite.DatabaseSync;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
diagnostics.warnings.push(`SQLite enrichment unavailable: ${error.code ?? error.message}`);
|
|
86
|
+
return null;
|
|
87
|
+
} finally {
|
|
88
|
+
process.emitWarning = originalEmitWarning;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readStateValue(db, key) {
|
|
93
|
+
const row = db.prepare('select value from ItemTable where key = ?').get(key);
|
|
94
|
+
if (!row?.value) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return safeJson(Buffer.from(row.value).toString('utf8'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sessionIdFromResource(resource) {
|
|
102
|
+
const encoded = String(resource ?? '')
|
|
103
|
+
.split('/')
|
|
104
|
+
.pop();
|
|
105
|
+
if (!encoded) {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return Buffer.from(encoded, 'base64').toString('utf8');
|
|
111
|
+
} catch {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function timestampFromMillis(value) {
|
|
117
|
+
const millis = Number(value);
|
|
118
|
+
return Number.isFinite(millis) && millis > 0 ? new Date(millis).toISOString() : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function locationLabel(location) {
|
|
122
|
+
const normalized = String(location ?? '').toLowerCase();
|
|
123
|
+
if (normalized === 'panel') {
|
|
124
|
+
return 'Chat Panel';
|
|
125
|
+
}
|
|
126
|
+
if (normalized === 'editor') {
|
|
127
|
+
return 'Editor';
|
|
128
|
+
}
|
|
129
|
+
if (normalized === 'terminal') {
|
|
130
|
+
return 'Terminal';
|
|
131
|
+
}
|
|
132
|
+
return location ? String(location) : 'Chat Panel';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function statusLabel(status, lastResponseState) {
|
|
136
|
+
if (Number(status) === 2) {
|
|
137
|
+
return 'Running';
|
|
138
|
+
}
|
|
139
|
+
if (Number(lastResponseState) === 2) {
|
|
140
|
+
return 'Error';
|
|
141
|
+
}
|
|
142
|
+
return 'Idle';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readWorkspaceState(workspaceDir) {
|
|
146
|
+
if (!DatabaseSync) {
|
|
147
|
+
return new Map();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const stateDb = join(workspaceDir, 'state.vscdb');
|
|
151
|
+
if (!existsSync(stateDb)) {
|
|
152
|
+
return new Map();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
diagnostics.scannedStateDbs += 1;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const db = new DatabaseSync(stateDb, { readOnly: true });
|
|
159
|
+
const chatIndex = readStateValue(db, 'chat.ChatSessionStore.index');
|
|
160
|
+
const agentModelCache = readStateValue(db, 'agentSessions.model.cache');
|
|
161
|
+
const agentStateCache = readStateValue(db, 'agentSessions.state.cache');
|
|
162
|
+
db.close();
|
|
163
|
+
|
|
164
|
+
const bySession = new Map();
|
|
165
|
+
const entries = Object.values(chatIndex?.entries ?? {});
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (!entry?.sessionId) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
bySession.set(entry.sessionId, {
|
|
172
|
+
sourcePath: stateDb,
|
|
173
|
+
keys: ['chat.ChatSessionStore.index'],
|
|
174
|
+
title: entry.title,
|
|
175
|
+
initialLocation: entry.initialLocation,
|
|
176
|
+
permissionLevel: entry.permissionLevel,
|
|
177
|
+
hasPendingEdits: Boolean(entry.hasPendingEdits),
|
|
178
|
+
isExternal: Boolean(entry.isExternal),
|
|
179
|
+
lastResponseState: entry.lastResponseState,
|
|
180
|
+
createdAt: timestampFromMillis(entry.timing?.created),
|
|
181
|
+
lastActivityAt: timestampFromMillis(
|
|
182
|
+
entry.timing?.lastRequestEnded ??
|
|
183
|
+
entry.lastMessageDate ??
|
|
184
|
+
entry.timing?.lastRequestStarted,
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const agent of Array.isArray(agentModelCache) ? agentModelCache : []) {
|
|
190
|
+
const sessionId = sessionIdFromResource(agent.resource);
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const existing = bySession.get(sessionId) ?? { sourcePath: stateDb, keys: [] };
|
|
196
|
+
bySession.set(sessionId, {
|
|
197
|
+
...existing,
|
|
198
|
+
keys: [...new Set([...(existing.keys ?? []), 'agentSessions.model.cache'])],
|
|
199
|
+
label: agent.label ?? existing.label,
|
|
200
|
+
sessionType: agent.providerLabel ?? existing.sessionType,
|
|
201
|
+
status: statusLabel(agent.status, existing.lastResponseState),
|
|
202
|
+
resource: agent.resource,
|
|
203
|
+
createdAt: existing.createdAt || timestampFromMillis(agent.timing?.created),
|
|
204
|
+
lastActivityAt:
|
|
205
|
+
existing.lastActivityAt ||
|
|
206
|
+
timestampFromMillis(agent.timing?.lastRequestEnded ?? agent.timing?.lastRequestStarted),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const readState of Array.isArray(agentStateCache) ? agentStateCache : []) {
|
|
211
|
+
const sessionId = sessionIdFromResource(readState.resource);
|
|
212
|
+
const existing = bySession.get(sessionId);
|
|
213
|
+
if (!sessionId || !existing) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
bySession.set(sessionId, {
|
|
218
|
+
...existing,
|
|
219
|
+
keys: [...new Set([...(existing.keys ?? []), 'agentSessions.state.cache'])],
|
|
220
|
+
readAt: timestampFromMillis(readState.read),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return bySession;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
diagnostics.warnings.push(`${stateDb}: SQLite enrichment skipped: ${error.message}`);
|
|
227
|
+
return new Map();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readJsonl(file) {
|
|
232
|
+
if (!existsSync(file)) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return readFileSync(file, 'utf8')
|
|
237
|
+
.split(/\r?\n/)
|
|
238
|
+
.map((line) => line.trim())
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.map((line) => safeJson(line))
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function listDirs(dir) {
|
|
245
|
+
if (!existsSync(dir)) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return readdirSync(dir)
|
|
250
|
+
.map((entry) => join(dir, entry))
|
|
251
|
+
.filter((path) => statSync(path).isDirectory());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function listFiles(dir, suffix) {
|
|
255
|
+
if (!existsSync(dir)) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return readdirSync(dir)
|
|
260
|
+
.map((entry) => join(dir, entry))
|
|
261
|
+
.filter((path) => statSync(path).isFile() && path.endsWith(suffix));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function costUsd(model, tokens) {
|
|
265
|
+
return costUsdForTokens(model, tokens, pricing, fallbackPricingModel);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function costBreakdownUsd(model, tokens) {
|
|
269
|
+
return costBreakdownUsdForTokens(model, tokens, pricing, fallbackPricingModel);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function transcriptAvailability(workspaceDir, sessionId) {
|
|
273
|
+
const sourcePath = join(workspaceDir, 'GitHub.copilot-chat', 'transcripts', `${sessionId}.jsonl`);
|
|
274
|
+
|
|
275
|
+
if (!existsSync(sourcePath)) {
|
|
276
|
+
return {
|
|
277
|
+
available: false,
|
|
278
|
+
sourcePath: '',
|
|
279
|
+
eventCount: 0,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const eventCount = readJsonl(sourcePath).length;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
available: true,
|
|
287
|
+
sourcePath,
|
|
288
|
+
eventCount,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function numericAttr(attrs, names) {
|
|
293
|
+
for (const name of names) {
|
|
294
|
+
const value = Number(attrs?.[name] ?? 0);
|
|
295
|
+
if (Number.isFinite(value) && value > 0) {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function llmTokenFields(event) {
|
|
304
|
+
const inputTokens = Number(event.attrs?.inputTokens ?? 0);
|
|
305
|
+
const outputTokens = Number(event.attrs?.outputTokens ?? 0);
|
|
306
|
+
const rawCachedInputTokens = numericAttr(event.attrs, [
|
|
307
|
+
'cachedTokens',
|
|
308
|
+
'cachedInputTokens',
|
|
309
|
+
'cacheReadTokens',
|
|
310
|
+
]);
|
|
311
|
+
const cachedInputTokens = Math.min(inputTokens, rawCachedInputTokens);
|
|
312
|
+
const cacheWriteTokens = numericAttr(event.attrs, ['cacheWriteTokens', 'cachedWriteTokens']);
|
|
313
|
+
const billableInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
inputTokens,
|
|
317
|
+
billableInputTokens,
|
|
318
|
+
rawCachedInputTokens,
|
|
319
|
+
cachedInputTokens,
|
|
320
|
+
cacheWriteTokens,
|
|
321
|
+
outputTokens,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function cacheTokenAuditFromLlmRequests(llmRequests) {
|
|
326
|
+
return llmRequests.reduce(
|
|
327
|
+
(audit, event) => {
|
|
328
|
+
const tokenFields = llmTokenFields(event);
|
|
329
|
+
|
|
330
|
+
audit.modelCalls += 1;
|
|
331
|
+
audit.rawInputTokens += tokenFields.inputTokens;
|
|
332
|
+
audit.normalInputTokens += tokenFields.billableInputTokens;
|
|
333
|
+
audit.cachedInputTokens += tokenFields.cachedInputTokens;
|
|
334
|
+
audit.cacheWriteTokens += tokenFields.cacheWriteTokens;
|
|
335
|
+
audit.outputTokens += tokenFields.outputTokens;
|
|
336
|
+
|
|
337
|
+
if (tokenFields.rawCachedInputTokens > 0) {
|
|
338
|
+
audit.callsWithCachedTokens += 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (tokenFields.rawCachedInputTokens > tokenFields.inputTokens) {
|
|
342
|
+
audit.invalidCachedTokenSplits += 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const rawInputShare =
|
|
346
|
+
tokenFields.inputTokens > 0 ? tokenFields.cachedInputTokens / tokenFields.inputTokens : 0;
|
|
347
|
+
audit.maxCachedInputShare = Math.max(audit.maxCachedInputShare, rawInputShare);
|
|
348
|
+
|
|
349
|
+
return audit;
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
modelCalls: 0,
|
|
353
|
+
callsWithCachedTokens: 0,
|
|
354
|
+
invalidCachedTokenSplits: 0,
|
|
355
|
+
rawInputTokens: 0,
|
|
356
|
+
normalInputTokens: 0,
|
|
357
|
+
cachedInputTokens: 0,
|
|
358
|
+
cacheWriteTokens: 0,
|
|
359
|
+
outputTokens: 0,
|
|
360
|
+
maxCachedInputShare: 0,
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function mergeCacheTokenAudits(audits) {
|
|
366
|
+
return audits.reduce(
|
|
367
|
+
(total, audit) => ({
|
|
368
|
+
modelCalls: total.modelCalls + audit.modelCalls,
|
|
369
|
+
callsWithCachedTokens: total.callsWithCachedTokens + audit.callsWithCachedTokens,
|
|
370
|
+
invalidCachedTokenSplits: total.invalidCachedTokenSplits + audit.invalidCachedTokenSplits,
|
|
371
|
+
rawInputTokens: total.rawInputTokens + audit.rawInputTokens,
|
|
372
|
+
normalInputTokens: total.normalInputTokens + audit.normalInputTokens,
|
|
373
|
+
cachedInputTokens: total.cachedInputTokens + audit.cachedInputTokens,
|
|
374
|
+
cacheWriteTokens: total.cacheWriteTokens + audit.cacheWriteTokens,
|
|
375
|
+
outputTokens: total.outputTokens + audit.outputTokens,
|
|
376
|
+
maxCachedInputShare: Math.max(total.maxCachedInputShare, audit.maxCachedInputShare),
|
|
377
|
+
}),
|
|
378
|
+
{
|
|
379
|
+
modelCalls: 0,
|
|
380
|
+
callsWithCachedTokens: 0,
|
|
381
|
+
invalidCachedTokenSplits: 0,
|
|
382
|
+
rawInputTokens: 0,
|
|
383
|
+
normalInputTokens: 0,
|
|
384
|
+
cachedInputTokens: 0,
|
|
385
|
+
cacheWriteTokens: 0,
|
|
386
|
+
outputTokens: 0,
|
|
387
|
+
maxCachedInputShare: 0,
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function eventModelCostFields(rawModel, tokenFields) {
|
|
393
|
+
const normalizedModel = normalizeModel(rawModel, pricing);
|
|
394
|
+
const pricingModel = pricingModelForModel(normalizedModel, pricing, fallbackPricingModel);
|
|
395
|
+
const tokens = {
|
|
396
|
+
input: tokenFields.billableInputTokens,
|
|
397
|
+
cachedInput: tokenFields.cachedInputTokens,
|
|
398
|
+
cacheWrite: tokenFields.cacheWriteTokens,
|
|
399
|
+
output: tokenFields.outputTokens,
|
|
400
|
+
};
|
|
401
|
+
const costBreakdown = costBreakdownUsd(pricingModel, tokens);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
model: normalizedModel,
|
|
405
|
+
rawModel:
|
|
406
|
+
String(rawModel ?? '')
|
|
407
|
+
.replace(/^copilot\//i, '')
|
|
408
|
+
.trim() || 'unknown',
|
|
409
|
+
pricingModel,
|
|
410
|
+
pricingTier: costBreakdown.tier,
|
|
411
|
+
totalTokens: tokenFields.inputTokens + tokenFields.outputTokens + tokenFields.cacheWriteTokens,
|
|
412
|
+
estimatedCost: { usd: costBreakdown.total, eur: costBreakdown.total * usdToEur },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseMaybeJson(value) {
|
|
417
|
+
if (typeof value !== 'string') {
|
|
418
|
+
return value ?? null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return safeJson(value) ?? value;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function charLength(value) {
|
|
425
|
+
if (value === undefined || value === null) {
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return typeof value === 'string' ? value.length : JSON.stringify(value).length;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseContentFile(file) {
|
|
433
|
+
const envelope = safeJson(readFileSync(file, 'utf8'));
|
|
434
|
+
const content = parseMaybeJson(envelope?.content ?? envelope);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
file,
|
|
438
|
+
content,
|
|
439
|
+
chars: charLength(content),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function modelCapabilityIndex(sessionDir) {
|
|
444
|
+
const file = join(sessionDir, 'models.json');
|
|
445
|
+
if (!existsSync(file)) {
|
|
446
|
+
return new Map();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const parsed = safeJson(readFileSync(file, 'utf8'));
|
|
450
|
+
const models = Array.isArray(parsed) ? parsed : [];
|
|
451
|
+
const index = new Map();
|
|
452
|
+
|
|
453
|
+
for (const model of models) {
|
|
454
|
+
const keys = [model?.id, model?.name, model?.version, model?.capabilities?.family]
|
|
455
|
+
.filter(Boolean)
|
|
456
|
+
.map(modelKey);
|
|
457
|
+
|
|
458
|
+
for (const key of keys) {
|
|
459
|
+
if (key) {
|
|
460
|
+
index.set(key, model);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return index;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function modelCapabilityFor(rawModel, capabilityIndex) {
|
|
469
|
+
const key = modelKey(rawModel);
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
capabilityIndex.get(key) ??
|
|
473
|
+
[...capabilityIndex.entries()].find(([candidate]) => key.includes(candidate))?.[1] ??
|
|
474
|
+
null
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function modelLimitSummaries(sessionDir, llmRequests) {
|
|
479
|
+
const capabilityIndex = modelCapabilityIndex(sessionDir);
|
|
480
|
+
if (!capabilityIndex.size || !llmRequests.length) {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const byModel = new Map();
|
|
485
|
+
|
|
486
|
+
for (const event of llmRequests) {
|
|
487
|
+
const rawModel = String(event.attrs?.model ?? '')
|
|
488
|
+
.replace(/^copilot\//i, '')
|
|
489
|
+
.trim();
|
|
490
|
+
const displayModel = normalizeModel(rawModel, pricing);
|
|
491
|
+
const capability = modelCapabilityFor(rawModel || displayModel, capabilityIndex);
|
|
492
|
+
const limits = capability?.capabilities?.limits ?? {};
|
|
493
|
+
const supports = capability?.capabilities?.supports ?? {};
|
|
494
|
+
const current = byModel.get(displayModel) ?? {
|
|
495
|
+
model: displayModel,
|
|
496
|
+
rawModels: new Set(),
|
|
497
|
+
modelId: capability?.id ?? rawModel,
|
|
498
|
+
vendor: capability?.vendor ?? '',
|
|
499
|
+
tokenizer: capability?.capabilities?.tokenizer ?? '',
|
|
500
|
+
contextWindowTokens: Number(limits.max_context_window_tokens ?? 0) || 0,
|
|
501
|
+
promptLimitTokens: Number(limits.max_prompt_tokens ?? 0) || 0,
|
|
502
|
+
outputLimitTokens: Number(limits.max_output_tokens ?? 0) || 0,
|
|
503
|
+
supportedReasoningEfforts: Array.isArray(supports.reasoning_effort)
|
|
504
|
+
? supports.reasoning_effort
|
|
505
|
+
: [],
|
|
506
|
+
supportedEndpoints: Array.isArray(capability?.supported_endpoints)
|
|
507
|
+
? capability.supported_endpoints
|
|
508
|
+
: [],
|
|
509
|
+
modelPickerEnabled: Boolean(capability?.model_picker_enabled),
|
|
510
|
+
isChatDefault: Boolean(capability?.is_chat_default),
|
|
511
|
+
isChatFallback: Boolean(capability?.is_chat_fallback),
|
|
512
|
+
modelCalls: 0,
|
|
513
|
+
largestRawInputTokens: 0,
|
|
514
|
+
totalRawInputTokens: 0,
|
|
515
|
+
largestOutputTokens: 0,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
current.rawModels.add(rawModel || 'unknown');
|
|
519
|
+
current.modelCalls += 1;
|
|
520
|
+
current.largestRawInputTokens = Math.max(
|
|
521
|
+
current.largestRawInputTokens,
|
|
522
|
+
Number(event.attrs?.inputTokens ?? 0),
|
|
523
|
+
);
|
|
524
|
+
current.totalRawInputTokens += Number(event.attrs?.inputTokens ?? 0);
|
|
525
|
+
current.largestOutputTokens = Math.max(
|
|
526
|
+
current.largestOutputTokens,
|
|
527
|
+
Number(event.attrs?.outputTokens ?? 0),
|
|
528
|
+
);
|
|
529
|
+
byModel.set(displayModel, current);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return [...byModel.values()].map((summary) => ({
|
|
533
|
+
...summary,
|
|
534
|
+
rawModels: [...summary.rawModels],
|
|
535
|
+
promptLimitShare:
|
|
536
|
+
summary.promptLimitTokens > 0
|
|
537
|
+
? summary.largestRawInputTokens / summary.promptLimitTokens
|
|
538
|
+
: null,
|
|
539
|
+
contextWindowShare:
|
|
540
|
+
summary.contextWindowTokens > 0
|
|
541
|
+
? summary.largestRawInputTokens / summary.contextWindowTokens
|
|
542
|
+
: null,
|
|
543
|
+
repeatedInputFactor:
|
|
544
|
+
summary.largestRawInputTokens > 0
|
|
545
|
+
? summary.totalRawInputTokens / summary.largestRawInputTokens
|
|
546
|
+
: 0,
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function toolName(tool) {
|
|
551
|
+
return String(tool?.function?.name ?? tool?.name ?? tool?.toolName ?? tool?.id ?? 'unknown_tool');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function toolSchemaSize(tool) {
|
|
555
|
+
const descriptionChars = charLength(tool?.function?.description ?? tool?.description);
|
|
556
|
+
const parameterChars = charLength(
|
|
557
|
+
tool?.function?.parameters ?? tool?.parameters ?? tool?.input_schema,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
name: toolName(tool),
|
|
562
|
+
descriptionChars,
|
|
563
|
+
parameterChars,
|
|
564
|
+
totalChars: charLength(tool),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function requestOptions(event) {
|
|
569
|
+
const parsed = parseMaybeJson(event.attrs?.requestOptions);
|
|
570
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function reasoningEffort(event) {
|
|
574
|
+
return String(requestOptions(event)?.reasoning?.effort ?? '').trim();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function textVerbosity(event) {
|
|
578
|
+
return String(requestOptions(event)?.text?.verbosity ?? '').trim();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function requestShapeMetadata(event) {
|
|
582
|
+
const shape = parseMaybeJson(event.attrs?.requestShape);
|
|
583
|
+
|
|
584
|
+
if (!shape || typeof shape !== 'object') {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
api: shape.api ? String(shape.api) : '',
|
|
590
|
+
inputItemCount: Number(shape.inputItemCount ?? 0),
|
|
591
|
+
inputItemTypes: Array.isArray(shape.inputItemTypes)
|
|
592
|
+
? shape.inputItemTypes.filter(Boolean).map(String)
|
|
593
|
+
: [],
|
|
594
|
+
hasPreviousResponseId: Boolean(shape.hasPreviousResponseId),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function requestShapeSummary(event) {
|
|
599
|
+
const shape = requestShapeMetadata(event);
|
|
600
|
+
if (!shape) {
|
|
601
|
+
return '';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const parts = [
|
|
605
|
+
shape.api ? `api: ${shape.api}` : '',
|
|
606
|
+
shape.inputItemCount
|
|
607
|
+
? `${shape.inputItemCount.toLocaleString()} input item${shape.inputItemCount === 1 ? '' : 's'}`
|
|
608
|
+
: '',
|
|
609
|
+
shape.inputItemTypes.length ? `types: ${shape.inputItemTypes.join(', ')}` : '',
|
|
610
|
+
shape.hasPreviousResponseId ? 'continues previous response' : '',
|
|
611
|
+
].filter(Boolean);
|
|
612
|
+
|
|
613
|
+
return parts.join(' · ');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function countedValues(values) {
|
|
617
|
+
const counts = new Map();
|
|
618
|
+
|
|
619
|
+
for (const value of values.filter(Boolean)) {
|
|
620
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return [...counts.entries()]
|
|
624
|
+
.map(([value, count]) => ({ value, count }))
|
|
625
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function toolPayloadSummary(toolEvents) {
|
|
629
|
+
const byName = new Map();
|
|
630
|
+
|
|
631
|
+
for (const event of toolEvents) {
|
|
632
|
+
const name = String(
|
|
633
|
+
event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type ?? 'tool',
|
|
634
|
+
);
|
|
635
|
+
const current = byName.get(name) ?? { name, calls: 0, argsChars: 0, resultChars: 0 };
|
|
636
|
+
current.calls += 1;
|
|
637
|
+
current.argsChars += charLength(
|
|
638
|
+
event.attrs?.args ??
|
|
639
|
+
event.attrs?.arguments ??
|
|
640
|
+
event.attrs?.input ??
|
|
641
|
+
event.data?.args ??
|
|
642
|
+
event.data?.arguments ??
|
|
643
|
+
event.data?.input,
|
|
644
|
+
);
|
|
645
|
+
current.resultChars += charLength(
|
|
646
|
+
event.attrs?.result ??
|
|
647
|
+
event.attrs?.output ??
|
|
648
|
+
event.attrs?.stdout ??
|
|
649
|
+
event.data?.result ??
|
|
650
|
+
event.data?.output ??
|
|
651
|
+
event.data?.stdout,
|
|
652
|
+
);
|
|
653
|
+
byName.set(name, current);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return [...byName.values()]
|
|
657
|
+
.sort(
|
|
658
|
+
(a, b) =>
|
|
659
|
+
b.resultChars + b.argsChars - (a.resultChars + a.argsChars) || a.name.localeCompare(b.name),
|
|
660
|
+
)
|
|
661
|
+
.slice(0, 12);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function requestPayloadSummary(sessionDir, llmRequests, toolEvents) {
|
|
665
|
+
const systemPromptFiles = [
|
|
666
|
+
...new Set(llmRequests.map((event) => event.attrs?.systemPromptFile).filter(Boolean)),
|
|
667
|
+
];
|
|
668
|
+
const toolsFiles = [
|
|
669
|
+
...new Set(llmRequests.map((event) => event.attrs?.toolsFile).filter(Boolean)),
|
|
670
|
+
];
|
|
671
|
+
const systemPrompts = systemPromptFiles
|
|
672
|
+
.map((file) => join(sessionDir, file))
|
|
673
|
+
.filter(existsSync)
|
|
674
|
+
.map(parseContentFile);
|
|
675
|
+
const toolFileSummaries = toolsFiles
|
|
676
|
+
.map((file) => join(sessionDir, file))
|
|
677
|
+
.filter(existsSync)
|
|
678
|
+
.map(parseContentFile);
|
|
679
|
+
const tools = toolFileSummaries.flatMap((summary) =>
|
|
680
|
+
Array.isArray(summary.content) ? summary.content : [],
|
|
681
|
+
);
|
|
682
|
+
const toolSchemas = tools.map(toolSchemaSize);
|
|
683
|
+
const mcpToolNames = toolSchemas
|
|
684
|
+
.map((tool) => tool.name)
|
|
685
|
+
.filter((name) => name.startsWith('mcp_'));
|
|
686
|
+
const reasoningEfforts = countedValues(llmRequests.map(reasoningEffort)).map(
|
|
687
|
+
({ value, count }) => ({
|
|
688
|
+
effort: value,
|
|
689
|
+
count,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
const subagentLogCount = listFiles(sessionDir, '.jsonl').filter((file) =>
|
|
693
|
+
basename(file).startsWith('runSubagent-'),
|
|
694
|
+
).length;
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
systemPromptFiles: systemPrompts.length,
|
|
698
|
+
systemPromptChars: systemPrompts.reduce((sum, summary) => sum + summary.chars, 0),
|
|
699
|
+
toolSchemaFiles: toolFileSummaries.length,
|
|
700
|
+
toolSchemaChars: toolFileSummaries.reduce((sum, summary) => sum + summary.chars, 0),
|
|
701
|
+
toolCount: toolSchemas.length,
|
|
702
|
+
mcpToolCount: mcpToolNames.length,
|
|
703
|
+
mcpToolNames: [...new Set(mcpToolNames)].sort(),
|
|
704
|
+
largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 8),
|
|
705
|
+
modelCallsWithSystemPromptFile: llmRequests.filter((event) => event.attrs?.systemPromptFile)
|
|
706
|
+
.length,
|
|
707
|
+
modelCallsWithToolsFile: llmRequests.filter((event) => event.attrs?.toolsFile).length,
|
|
708
|
+
reasoningEfforts,
|
|
709
|
+
toolResultCharsByName: toolPayloadSummary(toolEvents),
|
|
710
|
+
subagentLogCount,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function contentSummaryFromCache(sessionDir, cache, file) {
|
|
715
|
+
if (!file) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (cache.has(file)) {
|
|
720
|
+
return cache.get(file);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const path = join(sessionDir, file);
|
|
724
|
+
const summary = existsSync(path) ? parseContentFile(path) : null;
|
|
725
|
+
cache.set(file, summary);
|
|
726
|
+
return summary;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function modelCallSetupPayloadFactory(sessionDir) {
|
|
730
|
+
const cache = new Map();
|
|
731
|
+
|
|
732
|
+
return (event) => {
|
|
733
|
+
if (event.type !== 'llm_request') {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const systemPromptFile = String(event.attrs?.systemPromptFile ?? '').trim();
|
|
738
|
+
const toolsFile = String(event.attrs?.toolsFile ?? '').trim();
|
|
739
|
+
|
|
740
|
+
if (!systemPromptFile && !toolsFile) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const systemPrompt = contentSummaryFromCache(sessionDir, cache, systemPromptFile);
|
|
745
|
+
const toolsSummary = contentSummaryFromCache(sessionDir, cache, toolsFile);
|
|
746
|
+
const tools = Array.isArray(toolsSummary?.content) ? toolsSummary.content : [];
|
|
747
|
+
const toolSchemas = tools.map(toolSchemaSize);
|
|
748
|
+
const mcpToolNames = toolSchemas
|
|
749
|
+
.map((tool) => tool.name)
|
|
750
|
+
.filter((name) => name.startsWith('mcp_'));
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
systemPromptFile,
|
|
754
|
+
systemPromptChars: systemPrompt?.chars ?? 0,
|
|
755
|
+
toolsFile,
|
|
756
|
+
toolSchemaChars: toolsSummary?.chars ?? 0,
|
|
757
|
+
toolCount: toolSchemas.length,
|
|
758
|
+
mcpToolCount: mcpToolNames.length,
|
|
759
|
+
mcpToolNames: [...new Set(mcpToolNames)].sort(),
|
|
760
|
+
largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 5),
|
|
761
|
+
};
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function debugEvidence(llmRequests, agentResponses) {
|
|
766
|
+
const inputSeries = llmRequests.map((event) => Number(event.attrs?.inputTokens ?? 0));
|
|
767
|
+
const outputCaps = [
|
|
768
|
+
...new Set(
|
|
769
|
+
llmRequests.map((event) => Number(event.attrs?.maxTokens ?? 0)).filter((value) => value > 0),
|
|
770
|
+
),
|
|
771
|
+
].sort((a, b) => a - b);
|
|
772
|
+
const maxInputTokens = Math.max(0, ...inputSeries);
|
|
773
|
+
const maxRequestTokens = Math.max(0, ...outputCaps);
|
|
774
|
+
const reasoningEvents = agentResponses.filter((event) =>
|
|
775
|
+
String(event.attrs?.reasoning ?? '').trim(),
|
|
776
|
+
).length;
|
|
777
|
+
const efforts = countedValues(llmRequests.map(reasoningEffort));
|
|
778
|
+
const primaryEffort = efforts[0]?.value ?? '';
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
reasoning: {
|
|
782
|
+
visible: reasoningEvents > 0 || Boolean(primaryEffort),
|
|
783
|
+
level: primaryEffort,
|
|
784
|
+
events: reasoningEvents,
|
|
785
|
+
source: primaryEffort
|
|
786
|
+
? 'llm_request.attrs.requestOptions.reasoning.effort'
|
|
787
|
+
: reasoningEvents > 0
|
|
788
|
+
? 'agent_response.attrs.reasoning'
|
|
789
|
+
: '',
|
|
790
|
+
help: primaryEffort
|
|
791
|
+
? 'VS Code Agent Debug Logs expose the request reasoning effort in llm_request.attrs.requestOptions.reasoning.effort.'
|
|
792
|
+
: reasoningEvents > 0
|
|
793
|
+
? 'VS Code debug logs include reasoning text on agent_response events, but no request reasoning effort was imported.'
|
|
794
|
+
: 'No reasoning text field was present on imported agent_response events.',
|
|
795
|
+
},
|
|
796
|
+
context: {
|
|
797
|
+
maxInputTokens,
|
|
798
|
+
maxRequestTokens,
|
|
799
|
+
outputCaps,
|
|
800
|
+
requestCapShare: maxRequestTokens > 0 ? maxInputTokens / maxRequestTokens : null,
|
|
801
|
+
source:
|
|
802
|
+
maxRequestTokens > 0
|
|
803
|
+
? 'llm_request.attrs.inputTokens and attrs.maxTokens'
|
|
804
|
+
: 'llm_request.attrs.inputTokens',
|
|
805
|
+
help:
|
|
806
|
+
maxRequestTokens > 0
|
|
807
|
+
? 'Compares the largest observed input token count with the request maxTokens field present in VS Code debug logs. This is an observed pressure signal, not a provider context-window guarantee.'
|
|
808
|
+
: 'Largest observed model input token count. The log did not include a request cap to compare against.',
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function modelBreakdownFromLlmRequests(llmRequests) {
|
|
814
|
+
const byModel = new Map();
|
|
815
|
+
|
|
816
|
+
for (const event of llmRequests) {
|
|
817
|
+
const rawModel = String(event.attrs?.model ?? 'unknown')
|
|
818
|
+
.replace(/^copilot\//i, '')
|
|
819
|
+
.trim();
|
|
820
|
+
const displayModel = normalizeModel(rawModel, pricing);
|
|
821
|
+
const current = byModel.get(displayModel) ?? {
|
|
822
|
+
model: displayModel,
|
|
823
|
+
rawModels: new Set(),
|
|
824
|
+
turns: 0,
|
|
825
|
+
tokens: { input: 0, cachedInput: 0, cacheWrite: 0, output: 0 },
|
|
826
|
+
cost: { usd: 0, eur: 0 },
|
|
827
|
+
costBreakdown: { inputUsd: 0, cachedInputUsd: 0, cacheWriteUsd: 0, outputUsd: 0 },
|
|
828
|
+
pricingTiers: new Set(),
|
|
829
|
+
pricingModel: pricingModelForModel(displayModel, pricing, fallbackPricingModel),
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
current.rawModels.add(rawModel || 'unknown');
|
|
833
|
+
current.turns += 1;
|
|
834
|
+
const tokenFields = llmTokenFields(event);
|
|
835
|
+
current.tokens.input += tokenFields.billableInputTokens;
|
|
836
|
+
current.tokens.cachedInput += tokenFields.cachedInputTokens;
|
|
837
|
+
current.tokens.cacheWrite += tokenFields.cacheWriteTokens;
|
|
838
|
+
current.tokens.output += tokenFields.outputTokens;
|
|
839
|
+
const callCost = costBreakdownUsd(current.pricingModel, {
|
|
840
|
+
input: tokenFields.billableInputTokens,
|
|
841
|
+
cachedInput: tokenFields.cachedInputTokens,
|
|
842
|
+
cacheWrite: tokenFields.cacheWriteTokens,
|
|
843
|
+
output: tokenFields.outputTokens,
|
|
844
|
+
});
|
|
845
|
+
current.costBreakdown.inputUsd += callCost.input;
|
|
846
|
+
current.costBreakdown.cachedInputUsd += callCost.cachedInput;
|
|
847
|
+
current.costBreakdown.cacheWriteUsd += callCost.cacheWrite;
|
|
848
|
+
current.costBreakdown.outputUsd += callCost.output;
|
|
849
|
+
current.pricingTiers.add(callCost.tier);
|
|
850
|
+
byModel.set(displayModel, current);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return [...byModel.values()].map((entry) => {
|
|
854
|
+
const usd =
|
|
855
|
+
entry.costBreakdown.inputUsd +
|
|
856
|
+
entry.costBreakdown.cachedInputUsd +
|
|
857
|
+
entry.costBreakdown.cacheWriteUsd +
|
|
858
|
+
entry.costBreakdown.outputUsd;
|
|
859
|
+
return {
|
|
860
|
+
...entry,
|
|
861
|
+
rawModels: [...entry.rawModels],
|
|
862
|
+
pricingTiers: [...entry.pricingTiers],
|
|
863
|
+
cost: { usd, eur: usd * usdToEur },
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function sessionModelLabel(modelBreakdown, fallbackModel) {
|
|
869
|
+
if (modelBreakdown.length === 1) {
|
|
870
|
+
return modelBreakdown[0].model;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (modelBreakdown.length > 1) {
|
|
874
|
+
return `Mixed (${modelBreakdown.map((entry) => entry.model).join(', ')})`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return normalizeModel(fallbackModel, pricing);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function estimateTokens(text) {
|
|
881
|
+
const compact = String(text ?? '').trim();
|
|
882
|
+
if (!compact) {
|
|
883
|
+
return 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return Math.max(1, Math.round(Math.max(compact.split(/\s+/).length * 1.35, compact.length / 4)));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function timestampForEvent(event) {
|
|
890
|
+
return event?.timestamp ?? new Date(Number(event?.ts ?? 0)).toISOString();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function eventDetail(event) {
|
|
894
|
+
if (event.type === 'llm_request') {
|
|
895
|
+
return `${event.attrs?.model ?? 'model'}: ${Number(event.attrs?.inputTokens ?? 0).toLocaleString()} raw in / ${Number(
|
|
896
|
+
event.attrs?.outputTokens ?? 0,
|
|
897
|
+
).toLocaleString()} out`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (String(event.type ?? '').includes('tool')) {
|
|
901
|
+
return String(event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (event.type === 'user_message') {
|
|
905
|
+
return String(event.attrs?.content ?? '').slice(0, 140);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (event.type === 'agent_response') {
|
|
909
|
+
return parseAssistantResponse(event.attrs?.response).slice(0, 140);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return String(event.attrs?.details ?? event.name ?? event.type ?? '').slice(0, 140);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function boundedText(value, maxLength = 260) {
|
|
916
|
+
const compact = String(value ?? '')
|
|
917
|
+
.replace(/\s+/g, ' ')
|
|
918
|
+
.trim();
|
|
919
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function summaryValue(value) {
|
|
923
|
+
if (value === undefined || value === null || value === '') {
|
|
924
|
+
return '';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (typeof value === 'string') {
|
|
928
|
+
return boundedText(value);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
932
|
+
return String(value);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return boundedText(JSON.stringify(compactObject(value)));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function sourceEstimatedCost(event) {
|
|
939
|
+
return summaryValue(event.attrs?.estimatedCost);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function sourceUsageFromNanoAiu(event) {
|
|
943
|
+
const nanoAiu = Number(event.attrs?.copilotUsageNanoAiu ?? 0);
|
|
944
|
+
if (!Number.isFinite(nanoAiu) || nanoAiu <= 0) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const credits = nanoAiu / 1_000_000_000;
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
nanoAiu,
|
|
952
|
+
credits,
|
|
953
|
+
usd: credits * 0.01,
|
|
954
|
+
modelCalls: 1,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function sourceUsageSummary(llmRequests) {
|
|
959
|
+
const usages = llmRequests.map(sourceUsageFromNanoAiu).filter(Boolean);
|
|
960
|
+
const nanoAiu = usages.reduce((sum, usage) => sum + usage.nanoAiu, 0);
|
|
961
|
+
const credits = nanoAiu / 1_000_000_000;
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
nanoAiu,
|
|
965
|
+
credits,
|
|
966
|
+
usd: credits * 0.01,
|
|
967
|
+
modelCalls: usages.length,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function compactObject(value) {
|
|
972
|
+
if (!value || typeof value !== 'object') {
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const result = {};
|
|
977
|
+
const skip = new Set(['content', 'response', 'result', 'output', 'stdout', 'stderr']);
|
|
978
|
+
|
|
979
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
980
|
+
if (skip.has(key)) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (nestedValue === undefined || nestedValue === null) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
result[key] =
|
|
989
|
+
typeof nestedValue === 'object'
|
|
990
|
+
? boundedText(JSON.stringify(nestedValue), 120)
|
|
991
|
+
: boundedText(nestedValue, 120);
|
|
992
|
+
|
|
993
|
+
if (Object.keys(result).length >= 6) {
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function eventAttributeSummary(event) {
|
|
1002
|
+
const attrs = event.attrs ?? {};
|
|
1003
|
+
const data = event.data ?? {};
|
|
1004
|
+
const candidates = [
|
|
1005
|
+
['category', attrs.category],
|
|
1006
|
+
['source', attrs.source],
|
|
1007
|
+
['model', attrs.model],
|
|
1008
|
+
['debugName', attrs.debugName],
|
|
1009
|
+
['inputTokens', attrs.inputTokens],
|
|
1010
|
+
['cachedTokens', attrs.cachedTokens ?? attrs.cachedInputTokens ?? attrs.cacheReadTokens],
|
|
1011
|
+
['cacheWriteTokens', attrs.cacheWriteTokens ?? attrs.cachedWriteTokens],
|
|
1012
|
+
['outputTokens', attrs.outputTokens],
|
|
1013
|
+
['sourceEstimatedCost', attrs.estimatedCost],
|
|
1014
|
+
['copilotUsageNanoAiu', attrs.copilotUsageNanoAiu],
|
|
1015
|
+
['reasoningEffort', event.type === 'llm_request' ? reasoningEffort(event) : undefined],
|
|
1016
|
+
['textVerbosity', event.type === 'llm_request' ? textVerbosity(event) : undefined],
|
|
1017
|
+
['requestShape', event.type === 'llm_request' ? requestShapeSummary(event) : undefined],
|
|
1018
|
+
['maxTokens', attrs.maxTokens],
|
|
1019
|
+
['ttft', attrs.ttft],
|
|
1020
|
+
['systemPromptFile', attrs.systemPromptFile],
|
|
1021
|
+
['toolsFile', attrs.toolsFile],
|
|
1022
|
+
['vscodeVersion', attrs.vscodeVersion],
|
|
1023
|
+
['copilotVersion', attrs.copilotVersion],
|
|
1024
|
+
['responseId', attrs.responseId],
|
|
1025
|
+
['logVersion', event.v],
|
|
1026
|
+
['toolName', data.toolName ?? attrs.toolName],
|
|
1027
|
+
['details', attrs.details],
|
|
1028
|
+
['content', attrs.content],
|
|
1029
|
+
[
|
|
1030
|
+
'response',
|
|
1031
|
+
event.type === 'agent_response' ? parseAssistantResponse(attrs.response) : undefined,
|
|
1032
|
+
],
|
|
1033
|
+
['data', data && Object.keys(data).length ? data : undefined],
|
|
1034
|
+
];
|
|
1035
|
+
|
|
1036
|
+
const fields = [];
|
|
1037
|
+
const seen = new Set();
|
|
1038
|
+
|
|
1039
|
+
for (const [label, value] of candidates) {
|
|
1040
|
+
const summarized = summaryValue(value);
|
|
1041
|
+
|
|
1042
|
+
if (!summarized || seen.has(label)) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
fields.push({ label, value: summarized });
|
|
1047
|
+
seen.add(label);
|
|
1048
|
+
|
|
1049
|
+
if (fields.length >= 8) {
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return fields;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function capTraceEvents(events) {
|
|
1058
|
+
return events.slice(0, traceEventLimit);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function workspaceName(workspaceDir) {
|
|
1062
|
+
const workspaceJson = join(workspaceDir, 'workspace.json');
|
|
1063
|
+
const raw = existsSync(workspaceJson) ? safeJson(readFileSync(workspaceJson, 'utf8')) : null;
|
|
1064
|
+
const folder = raw?.folder ? decodeURIComponent(String(raw.folder).replace(/^file:\/+/, '')) : '';
|
|
1065
|
+
return folder ? basename(folder) : basename(workspaceDir);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function parseAssistantResponse(raw) {
|
|
1069
|
+
const parsed = typeof raw === 'string' ? safeJson(raw) : raw;
|
|
1070
|
+
if (!Array.isArray(parsed)) {
|
|
1071
|
+
return String(raw ?? '');
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return parsed
|
|
1075
|
+
.flatMap((message) => message?.parts ?? [])
|
|
1076
|
+
.map((part) => {
|
|
1077
|
+
const content =
|
|
1078
|
+
typeof part?.content === 'string'
|
|
1079
|
+
? (safeJson(part.content) ?? part.content)
|
|
1080
|
+
: part?.content;
|
|
1081
|
+
return typeof content === 'object' && content?.text ? content.text : String(content ?? '');
|
|
1082
|
+
})
|
|
1083
|
+
.filter(Boolean)
|
|
1084
|
+
.join('\n');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export function sessionFromDebugLog(sessionDir, workspaceDir) {
|
|
1088
|
+
const main = readJsonl(join(sessionDir, 'main.jsonl'));
|
|
1089
|
+
if (!main.length) {
|
|
1090
|
+
diagnostics.skippedEmptyDebugLogs += 1;
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const sid = basename(sessionDir);
|
|
1095
|
+
const userMessages = main.filter((event) => event.type === 'user_message');
|
|
1096
|
+
const llmRequests = main.filter((event) => event.type === 'llm_request');
|
|
1097
|
+
const assistantEvents = main.filter(
|
|
1098
|
+
(event) => event.type === 'agent_response' || event.type === 'assistant.message',
|
|
1099
|
+
);
|
|
1100
|
+
const toolEvents = main.filter((event) => String(event.type ?? '').includes('tool'));
|
|
1101
|
+
const errorEvents = main.filter((event) => event.status && event.status !== 'ok');
|
|
1102
|
+
|
|
1103
|
+
if (!userMessages.length && !llmRequests.length && !assistantEvents.length) {
|
|
1104
|
+
diagnostics.skippedEmptyDebugLogs += 1;
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const firstUserMessage = userMessages[0]?.attrs?.content ?? 'Untitled Copilot session';
|
|
1109
|
+
const modelBreakdown = modelBreakdownFromLlmRequests(llmRequests);
|
|
1110
|
+
const model = sessionModelLabel(
|
|
1111
|
+
modelBreakdown,
|
|
1112
|
+
llmRequests.find((event) => event.attrs?.model)?.attrs?.model,
|
|
1113
|
+
);
|
|
1114
|
+
const input = llmRequests.reduce(
|
|
1115
|
+
(sum, event) => sum + llmTokenFields(event).billableInputTokens,
|
|
1116
|
+
0,
|
|
1117
|
+
);
|
|
1118
|
+
const cachedInput = llmRequests.reduce(
|
|
1119
|
+
(sum, event) => sum + llmTokenFields(event).cachedInputTokens,
|
|
1120
|
+
0,
|
|
1121
|
+
);
|
|
1122
|
+
const cacheWrite = llmRequests.reduce(
|
|
1123
|
+
(sum, event) => sum + llmTokenFields(event).cacheWriteTokens,
|
|
1124
|
+
0,
|
|
1125
|
+
);
|
|
1126
|
+
const output = llmRequests.reduce((sum, event) => sum + llmTokenFields(event).outputTokens, 0);
|
|
1127
|
+
const tokens = { input, cachedInput, cacheWrite, output };
|
|
1128
|
+
const usd = modelBreakdown.length
|
|
1129
|
+
? modelBreakdown.reduce((sum, entry) => sum + entry.cost.usd, 0)
|
|
1130
|
+
: costUsd(model, tokens);
|
|
1131
|
+
const startEvent = main.find((event) => event.type === 'session_start') ?? main[0];
|
|
1132
|
+
const debugLogRuntime = {
|
|
1133
|
+
logVersion: Number(startEvent?.v ?? 0) || 0,
|
|
1134
|
+
vscodeVersion: String(startEvent?.attrs?.vscodeVersion ?? '').trim(),
|
|
1135
|
+
copilotVersion: String(startEvent?.attrs?.copilotVersion ?? '').trim(),
|
|
1136
|
+
};
|
|
1137
|
+
const lastEvent = main[main.length - 1];
|
|
1138
|
+
const startedAt =
|
|
1139
|
+
startEvent?.timestamp ??
|
|
1140
|
+
new Date(
|
|
1141
|
+
Number(startEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
|
|
1142
|
+
).toISOString();
|
|
1143
|
+
const endedAt =
|
|
1144
|
+
lastEvent?.timestamp ??
|
|
1145
|
+
new Date(
|
|
1146
|
+
Number(lastEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
|
|
1147
|
+
).toISOString();
|
|
1148
|
+
const evidence = debugEvidence(llmRequests, assistantEvents, main);
|
|
1149
|
+
const payload = requestPayloadSummary(sessionDir, llmRequests, toolEvents);
|
|
1150
|
+
const modelLimits = modelLimitSummaries(sessionDir, llmRequests);
|
|
1151
|
+
const cacheTokenAudit = cacheTokenAuditFromLlmRequests(llmRequests);
|
|
1152
|
+
const sourceUsage = sourceUsageSummary(llmRequests);
|
|
1153
|
+
const setupPayloadForEvent = modelCallSetupPayloadFactory(sessionDir);
|
|
1154
|
+
const transcript = transcriptAvailability(workspaceDir, sid);
|
|
1155
|
+
|
|
1156
|
+
if (transcript.available) {
|
|
1157
|
+
diagnostics.debugLogSessionsWithTranscripts += 1;
|
|
1158
|
+
diagnostics.transcriptEventsAvailable += transcript.eventCount;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (cacheTokenAudit.invalidCachedTokenSplits > 0) {
|
|
1162
|
+
diagnostics.warnings.push(
|
|
1163
|
+
`${sessionDir}: ${cacheTokenAudit.invalidCachedTokenSplits} model call(s) reported cachedTokens greater than inputTokens; cached input was clamped for pricing safety`,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const turns = [
|
|
1168
|
+
...userMessages.map((event) => ({
|
|
1169
|
+
role: 'user',
|
|
1170
|
+
text: String(event.attrs?.content ?? ''),
|
|
1171
|
+
tokens: estimateTokens(event.attrs?.content),
|
|
1172
|
+
})),
|
|
1173
|
+
...assistantEvents.map((event) => {
|
|
1174
|
+
const text =
|
|
1175
|
+
event.type === 'assistant.message'
|
|
1176
|
+
? event.data?.content
|
|
1177
|
+
: parseAssistantResponse(event.attrs?.response);
|
|
1178
|
+
return { role: 'assistant', text: String(text ?? ''), tokens: estimateTokens(text) };
|
|
1179
|
+
}),
|
|
1180
|
+
].filter((turn) => turn.text.trim());
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
id: sid,
|
|
1184
|
+
sourceKind: 'vscode-copilot-debug-log',
|
|
1185
|
+
tokenSource: llmRequests.length
|
|
1186
|
+
? 'llm_request_token_totals'
|
|
1187
|
+
: 'debug-log-visible-text-estimate',
|
|
1188
|
+
sessionType: 'Local',
|
|
1189
|
+
location: 'Chat Panel',
|
|
1190
|
+
status: 'Idle',
|
|
1191
|
+
title: firstUserMessage.slice(0, 80),
|
|
1192
|
+
firstPrompt: firstUserMessage.slice(0, 240),
|
|
1193
|
+
workspace: workspaceName(workspaceDir),
|
|
1194
|
+
sourcePath: sessionDir,
|
|
1195
|
+
...(debugLogRuntime.logVersion ||
|
|
1196
|
+
debugLogRuntime.vscodeVersion ||
|
|
1197
|
+
debugLogRuntime.copilotVersion
|
|
1198
|
+
? { debugLogRuntime }
|
|
1199
|
+
: {}),
|
|
1200
|
+
model,
|
|
1201
|
+
modelBreakdown,
|
|
1202
|
+
startedAt,
|
|
1203
|
+
endedAt,
|
|
1204
|
+
tags: ['debug-log', llmRequests.length ? 'llm-request-token-totals' : 'estimated-visible-text'],
|
|
1205
|
+
toolsUsed: [
|
|
1206
|
+
...new Set(
|
|
1207
|
+
toolEvents
|
|
1208
|
+
.map((event) => event.data?.toolName ?? event.attrs?.toolName ?? event.name)
|
|
1209
|
+
.filter(Boolean),
|
|
1210
|
+
),
|
|
1211
|
+
],
|
|
1212
|
+
tokens,
|
|
1213
|
+
cost: { usd, eur: usd * usdToEur },
|
|
1214
|
+
confidence: llmRequests.length ? 'exact' : 'estimated',
|
|
1215
|
+
traceSummary: {
|
|
1216
|
+
modelTurns: llmRequests.length,
|
|
1217
|
+
toolCalls: toolEvents.length,
|
|
1218
|
+
totalTokens: input + cachedInput + cacheWrite + output,
|
|
1219
|
+
errors: errorEvents.length,
|
|
1220
|
+
totalEvents: main.length,
|
|
1221
|
+
reasoningEvents: evidence.reasoning.events,
|
|
1222
|
+
maxInputTokens: evidence.context.maxInputTokens,
|
|
1223
|
+
maxRequestTokens: evidence.context.maxRequestTokens,
|
|
1224
|
+
reasoningEfforts: payload.reasoningEfforts,
|
|
1225
|
+
},
|
|
1226
|
+
cacheTokenAudit,
|
|
1227
|
+
...(sourceUsage.modelCalls > 0 ? { sourceUsage } : {}),
|
|
1228
|
+
transcript,
|
|
1229
|
+
advancedSignals: evidence,
|
|
1230
|
+
requestPayload: payload,
|
|
1231
|
+
modelLimits,
|
|
1232
|
+
traceEvents: capTraceEvents(
|
|
1233
|
+
main.map((event, index) => {
|
|
1234
|
+
const tokenFields =
|
|
1235
|
+
event.type === 'llm_request'
|
|
1236
|
+
? llmTokenFields(event)
|
|
1237
|
+
: {
|
|
1238
|
+
inputTokens: 0,
|
|
1239
|
+
billableInputTokens: 0,
|
|
1240
|
+
cachedInputTokens: 0,
|
|
1241
|
+
cacheWriteTokens: 0,
|
|
1242
|
+
outputTokens: 0,
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
const setupPayload = setupPayloadForEvent(event);
|
|
1246
|
+
const sourceUsage = sourceUsageFromNanoAiu(event);
|
|
1247
|
+
const requestShape = event.type === 'llm_request' ? requestShapeMetadata(event) : null;
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
index,
|
|
1251
|
+
timestamp: timestampForEvent(event),
|
|
1252
|
+
type: String(event.type ?? 'unknown'),
|
|
1253
|
+
name: String(event.name ?? event.type ?? 'unknown'),
|
|
1254
|
+
status: String(event.status ?? 'unknown'),
|
|
1255
|
+
detail: eventDetail(event),
|
|
1256
|
+
attributes: eventAttributeSummary(event),
|
|
1257
|
+
inputTokens: tokenFields.inputTokens,
|
|
1258
|
+
cachedInputTokens: tokenFields.cachedInputTokens,
|
|
1259
|
+
cacheWriteTokens: tokenFields.cacheWriteTokens,
|
|
1260
|
+
outputTokens: tokenFields.outputTokens,
|
|
1261
|
+
ttftMs: event.type === 'llm_request' ? Number(event.attrs?.ttft ?? 0) : 0,
|
|
1262
|
+
maxTokens: event.type === 'llm_request' ? Number(event.attrs?.maxTokens ?? 0) : 0,
|
|
1263
|
+
hasReasoning:
|
|
1264
|
+
event.type === 'agent_response' && Boolean(String(event.attrs?.reasoning ?? '').trim()),
|
|
1265
|
+
reasoningEffort: event.type === 'llm_request' ? reasoningEffort(event) : '',
|
|
1266
|
+
...(requestShape ? { requestShape } : {}),
|
|
1267
|
+
...(event.type === 'llm_request'
|
|
1268
|
+
? eventModelCostFields(event.attrs?.model, tokenFields)
|
|
1269
|
+
: {}),
|
|
1270
|
+
...(event.type === 'llm_request' && sourceEstimatedCost(event)
|
|
1271
|
+
? { sourceEstimatedCost: sourceEstimatedCost(event) }
|
|
1272
|
+
: {}),
|
|
1273
|
+
...(sourceUsage ? { sourceUsage } : {}),
|
|
1274
|
+
...(setupPayload ? { setupPayload } : {}),
|
|
1275
|
+
};
|
|
1276
|
+
}),
|
|
1277
|
+
),
|
|
1278
|
+
turns: turns.slice(0, 60),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
export function sessionFromChatSnapshot(file, workspaceDir) {
|
|
1283
|
+
const records = readJsonl(file);
|
|
1284
|
+
const snapshot =
|
|
1285
|
+
records.find((record) => record.kind === 0 && record.v?.requests)?.v ??
|
|
1286
|
+
records[0]?.v ??
|
|
1287
|
+
records[0];
|
|
1288
|
+
const requests = snapshot?.requests ?? [];
|
|
1289
|
+
|
|
1290
|
+
if (!requests.length) {
|
|
1291
|
+
diagnostics.skippedChatSnapshotsWithoutRequests += 1;
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const firstRequest = requests[0];
|
|
1296
|
+
const firstPrompt =
|
|
1297
|
+
firstRequest?.message?.text ?? snapshot?.customTitle ?? 'Untitled Copilot session';
|
|
1298
|
+
const model = normalizeModel(
|
|
1299
|
+
firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier,
|
|
1300
|
+
pricing,
|
|
1301
|
+
);
|
|
1302
|
+
const output = requests.reduce((sum, request) => sum + Number(request.completionTokens ?? 0), 0);
|
|
1303
|
+
const input = requests.reduce(
|
|
1304
|
+
(sum, request) => sum + estimateTokens(request?.message?.text ?? ''),
|
|
1305
|
+
0,
|
|
1306
|
+
);
|
|
1307
|
+
const tokens = { input, cachedInput: 0, cacheWrite: 0, output };
|
|
1308
|
+
const usd = costUsd(model, tokens);
|
|
1309
|
+
const pricingModel = pricingModelForModel(model, pricing, fallbackPricingModel);
|
|
1310
|
+
const startedAt = new Date(Number(snapshot.creationDate ?? statSync(file).mtimeMs)).toISOString();
|
|
1311
|
+
const endedAt = statSync(file).mtime.toISOString();
|
|
1312
|
+
|
|
1313
|
+
return {
|
|
1314
|
+
id: basename(file, '.jsonl'),
|
|
1315
|
+
sourceKind: 'vscode-chat-session-snapshot',
|
|
1316
|
+
tokenSource: 'chat-snapshot-output-plus-visible-input-estimate',
|
|
1317
|
+
sessionType: 'Local',
|
|
1318
|
+
location: 'Chat Panel',
|
|
1319
|
+
status: 'Idle',
|
|
1320
|
+
title: String(snapshot.customTitle ?? firstPrompt).slice(0, 80),
|
|
1321
|
+
firstPrompt: String(firstPrompt).slice(0, 240),
|
|
1322
|
+
workspace: workspaceName(workspaceDir),
|
|
1323
|
+
sourcePath: file,
|
|
1324
|
+
model,
|
|
1325
|
+
modelBreakdown: [
|
|
1326
|
+
{
|
|
1327
|
+
model,
|
|
1328
|
+
rawModels: [
|
|
1329
|
+
String(
|
|
1330
|
+
firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier ?? model,
|
|
1331
|
+
),
|
|
1332
|
+
],
|
|
1333
|
+
turns: requests.length,
|
|
1334
|
+
tokens,
|
|
1335
|
+
cost: { usd, eur: usd * usdToEur },
|
|
1336
|
+
pricingModel,
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
startedAt,
|
|
1340
|
+
endedAt,
|
|
1341
|
+
tags: ['chat-session', 'estimated-input'],
|
|
1342
|
+
toolsUsed: [],
|
|
1343
|
+
tokens,
|
|
1344
|
+
cost: { usd, eur: usd * usdToEur },
|
|
1345
|
+
confidence: 'estimated',
|
|
1346
|
+
traceSummary: {
|
|
1347
|
+
modelTurns: requests.length,
|
|
1348
|
+
toolCalls: 0,
|
|
1349
|
+
totalTokens: input + output,
|
|
1350
|
+
errors: 0,
|
|
1351
|
+
totalEvents: records.length,
|
|
1352
|
+
reasoningEvents: 0,
|
|
1353
|
+
maxInputTokens: input,
|
|
1354
|
+
maxRequestTokens: 0,
|
|
1355
|
+
},
|
|
1356
|
+
cacheTokenAudit: {
|
|
1357
|
+
modelCalls: 0,
|
|
1358
|
+
callsWithCachedTokens: 0,
|
|
1359
|
+
invalidCachedTokenSplits: 0,
|
|
1360
|
+
rawInputTokens: 0,
|
|
1361
|
+
normalInputTokens: 0,
|
|
1362
|
+
cachedInputTokens: 0,
|
|
1363
|
+
cacheWriteTokens: 0,
|
|
1364
|
+
outputTokens: 0,
|
|
1365
|
+
maxCachedInputShare: 0,
|
|
1366
|
+
},
|
|
1367
|
+
transcript: {
|
|
1368
|
+
available: false,
|
|
1369
|
+
sourcePath: '',
|
|
1370
|
+
eventCount: 0,
|
|
1371
|
+
},
|
|
1372
|
+
advancedSignals: {
|
|
1373
|
+
reasoning: {
|
|
1374
|
+
visible: false,
|
|
1375
|
+
level: '',
|
|
1376
|
+
events: 0,
|
|
1377
|
+
source: '',
|
|
1378
|
+
help: 'Chat snapshots do not expose agent_response reasoning text or a reasoning-level field.',
|
|
1379
|
+
},
|
|
1380
|
+
context: {
|
|
1381
|
+
maxInputTokens: input,
|
|
1382
|
+
maxRequestTokens: 0,
|
|
1383
|
+
outputCaps: [],
|
|
1384
|
+
requestCapShare: null,
|
|
1385
|
+
source: 'estimated visible chat text',
|
|
1386
|
+
help: 'Chat snapshots only provide visible text context here, so context pressure is not reliable for cost debugging.',
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
traceEvents: capTraceEvents(
|
|
1390
|
+
requests.flatMap((request, index) => {
|
|
1391
|
+
const rawRequestModel =
|
|
1392
|
+
request?.modelId ?? request?.inputState?.selectedModel?.identifier ?? model;
|
|
1393
|
+
const userInputTokens = estimateTokens(request?.message?.text ?? '');
|
|
1394
|
+
const assistantOutputTokens = Number(request.completionTokens ?? 0);
|
|
1395
|
+
|
|
1396
|
+
return [
|
|
1397
|
+
{
|
|
1398
|
+
index: index * 2,
|
|
1399
|
+
timestamp: startedAt,
|
|
1400
|
+
type: 'user_message',
|
|
1401
|
+
name: 'user_message',
|
|
1402
|
+
status: 'ok',
|
|
1403
|
+
detail: String(request?.message?.text ?? '').slice(0, 140),
|
|
1404
|
+
inputTokens: userInputTokens,
|
|
1405
|
+
cachedInputTokens: 0,
|
|
1406
|
+
cacheWriteTokens: 0,
|
|
1407
|
+
outputTokens: 0,
|
|
1408
|
+
...eventModelCostFields(rawRequestModel, {
|
|
1409
|
+
inputTokens: userInputTokens,
|
|
1410
|
+
billableInputTokens: userInputTokens,
|
|
1411
|
+
cachedInputTokens: 0,
|
|
1412
|
+
cacheWriteTokens: 0,
|
|
1413
|
+
outputTokens: 0,
|
|
1414
|
+
}),
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
index: index * 2 + 1,
|
|
1418
|
+
timestamp: endedAt,
|
|
1419
|
+
type: 'assistant_response',
|
|
1420
|
+
name: 'assistant_response',
|
|
1421
|
+
status: 'ok',
|
|
1422
|
+
detail: `${assistantOutputTokens.toLocaleString()} completion tokens`,
|
|
1423
|
+
inputTokens: 0,
|
|
1424
|
+
cachedInputTokens: 0,
|
|
1425
|
+
cacheWriteTokens: 0,
|
|
1426
|
+
outputTokens: assistantOutputTokens,
|
|
1427
|
+
...eventModelCostFields(rawRequestModel, {
|
|
1428
|
+
inputTokens: 0,
|
|
1429
|
+
billableInputTokens: 0,
|
|
1430
|
+
cachedInputTokens: 0,
|
|
1431
|
+
cacheWriteTokens: 0,
|
|
1432
|
+
outputTokens: assistantOutputTokens,
|
|
1433
|
+
}),
|
|
1434
|
+
},
|
|
1435
|
+
];
|
|
1436
|
+
}),
|
|
1437
|
+
),
|
|
1438
|
+
turns: requests
|
|
1439
|
+
.flatMap((request) => [
|
|
1440
|
+
{
|
|
1441
|
+
role: 'user',
|
|
1442
|
+
text: String(request?.message?.text ?? ''),
|
|
1443
|
+
tokens: estimateTokens(request?.message?.text),
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
role: 'assistant',
|
|
1447
|
+
text: (request?.response ?? [])
|
|
1448
|
+
.map((part) => part.value ?? part.generatedTitle ?? part.kind ?? '')
|
|
1449
|
+
.filter(Boolean)
|
|
1450
|
+
.join('\n'),
|
|
1451
|
+
tokens: Number(request.completionTokens ?? 0),
|
|
1452
|
+
},
|
|
1453
|
+
])
|
|
1454
|
+
.filter((turn) => turn.text.trim())
|
|
1455
|
+
.slice(0, 60),
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function enrichSessionFromWorkspaceState(session, stateBySessionId) {
|
|
1460
|
+
const state = stateBySessionId.get(session.id);
|
|
1461
|
+
if (!state) {
|
|
1462
|
+
return session;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
diagnostics.enrichedFromStateDbs += 1;
|
|
1466
|
+
|
|
1467
|
+
const title = String(state.title ?? state.label ?? session.title);
|
|
1468
|
+
return {
|
|
1469
|
+
...session,
|
|
1470
|
+
title: title.slice(0, 80),
|
|
1471
|
+
location: locationLabel(state.initialLocation ?? session.location),
|
|
1472
|
+
sessionType: state.sessionType ?? session.sessionType,
|
|
1473
|
+
status: state.status ?? statusLabel(undefined, state.lastResponseState),
|
|
1474
|
+
startedAt: state.createdAt || session.startedAt,
|
|
1475
|
+
endedAt: state.lastActivityAt || session.endedAt,
|
|
1476
|
+
tags: [
|
|
1477
|
+
...new Set(
|
|
1478
|
+
[
|
|
1479
|
+
...session.tags,
|
|
1480
|
+
'state-vscdb-enriched',
|
|
1481
|
+
state.hasPendingEdits ? 'pending-edits' : '',
|
|
1482
|
+
state.isExternal ? 'external' : '',
|
|
1483
|
+
].filter(Boolean),
|
|
1484
|
+
),
|
|
1485
|
+
],
|
|
1486
|
+
vscodeState: {
|
|
1487
|
+
sourcePath: state.sourcePath,
|
|
1488
|
+
keys: state.keys ?? [],
|
|
1489
|
+
title: state.title ?? '',
|
|
1490
|
+
label: state.label ?? '',
|
|
1491
|
+
resource: state.resource ?? '',
|
|
1492
|
+
initialLocation: state.initialLocation ?? '',
|
|
1493
|
+
permissionLevel: state.permissionLevel ?? '',
|
|
1494
|
+
hasPendingEdits: Boolean(state.hasPendingEdits),
|
|
1495
|
+
isExternal: Boolean(state.isExternal),
|
|
1496
|
+
lastResponseState: Number(state.lastResponseState ?? 0),
|
|
1497
|
+
readAt: state.readAt ?? '',
|
|
1498
|
+
createdAt: state.createdAt ?? '',
|
|
1499
|
+
lastActivityAt: state.lastActivityAt ?? '',
|
|
1500
|
+
},
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function parseWorkspace(workspaceDir) {
|
|
1505
|
+
diagnostics.scannedWorkspaces += 1;
|
|
1506
|
+
const stateBySessionId = readWorkspaceState(workspaceDir);
|
|
1507
|
+
const debugRoot = join(workspaceDir, 'GitHub.copilot-chat', 'debug-logs');
|
|
1508
|
+
const debugSessions = listDirs(debugRoot)
|
|
1509
|
+
.map((sessionDir) => sessionFromDebugLog(sessionDir, workspaceDir))
|
|
1510
|
+
.filter(Boolean)
|
|
1511
|
+
.map((session) => enrichSessionFromWorkspaceState(session, stateBySessionId));
|
|
1512
|
+
const debugIds = new Set(debugSessions.map((session) => session.id));
|
|
1513
|
+
diagnostics.importedDebugLogSessions += debugSessions.length;
|
|
1514
|
+
|
|
1515
|
+
const chatSessions = listFiles(join(workspaceDir, 'chatSessions'), '.jsonl')
|
|
1516
|
+
.map((file) => {
|
|
1517
|
+
const session = sessionFromChatSnapshot(file, workspaceDir);
|
|
1518
|
+
if (session && debugIds.has(session.id)) {
|
|
1519
|
+
diagnostics.skippedDuplicateChatSnapshots += 1;
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
return session ? enrichSessionFromWorkspaceState(session, stateBySessionId) : null;
|
|
1523
|
+
})
|
|
1524
|
+
.filter(Boolean);
|
|
1525
|
+
diagnostics.importedChatSnapshotSessions += chatSessions.length;
|
|
1526
|
+
|
|
1527
|
+
return [...debugSessions, ...chatSessions];
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function workspaceDirsFromUserDir(userDir) {
|
|
1531
|
+
const workspaceStorage = join(userDir, 'workspaceStorage');
|
|
1532
|
+
return listDirs(workspaceStorage);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Scan local VS Code Copilot storage and return the normalized app data model.
|
|
1537
|
+
* This function does not write files, which lets CLI, desktop, extension, and
|
|
1538
|
+
* local-server hosts decide how and where the result should be persisted.
|
|
1539
|
+
*/
|
|
1540
|
+
export async function scanVsCodeSessions(options = {}) {
|
|
1541
|
+
if (scanInProgress) {
|
|
1542
|
+
throw new Error('A VS Code session scan is already in progress in this process.');
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const configuredRoots = options.roots === undefined ? defaultCodeUserDirs() : options.roots;
|
|
1546
|
+
if (!Array.isArray(configuredRoots)) {
|
|
1547
|
+
throw new TypeError('roots must be an array of VS Code user-data or workspace-storage paths.');
|
|
1548
|
+
}
|
|
1549
|
+
const roots = [...new Set(configuredRoots.map((root) => resolve(String(root))))];
|
|
1550
|
+
const conversionRate = Number(options.usdToEur ?? process.env.USD_TO_EUR ?? 1);
|
|
1551
|
+
if (!Number.isFinite(conversionRate) || conversionRate <= 0) {
|
|
1552
|
+
throw new TypeError('usdToEur must be a positive number.');
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const generatedAt = options.generatedAt
|
|
1556
|
+
? new Date(options.generatedAt).toISOString()
|
|
1557
|
+
: new Date().toISOString();
|
|
1558
|
+
const previousDiagnostics = diagnostics;
|
|
1559
|
+
const previousDatabaseSync = DatabaseSync;
|
|
1560
|
+
const previousUsdToEur = usdToEur;
|
|
1561
|
+
|
|
1562
|
+
scanInProgress = true;
|
|
1563
|
+
diagnostics = createDiagnostics();
|
|
1564
|
+
diagnostics.scannedRoots = roots;
|
|
1565
|
+
usdToEur = conversionRate;
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
DatabaseSync = options.sqlite === false ? null : await loadSqliteSupport();
|
|
1569
|
+
const workspaceDirs = roots.flatMap((root) => {
|
|
1570
|
+
if (
|
|
1571
|
+
basename(dirname(root)) === 'workspaceStorage' ||
|
|
1572
|
+
existsSync(join(root, 'workspace.json'))
|
|
1573
|
+
) {
|
|
1574
|
+
return [root];
|
|
1575
|
+
}
|
|
1576
|
+
return workspaceDirsFromUserDir(root);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const sessions = workspaceDirs
|
|
1580
|
+
.flatMap(parseWorkspace)
|
|
1581
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
1582
|
+
const seenIds = new Set();
|
|
1583
|
+
for (const session of sessions) {
|
|
1584
|
+
if (seenIds.has(session.id)) {
|
|
1585
|
+
diagnostics.warnings.push(`Duplicate session id imported: ${session.id}`);
|
|
1586
|
+
}
|
|
1587
|
+
seenIds.add(session.id);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
schemaVersion: sessionDataSchemaVersion,
|
|
1592
|
+
generatedAt,
|
|
1593
|
+
pricingVersion,
|
|
1594
|
+
pricingSourceUrl,
|
|
1595
|
+
usdToEur,
|
|
1596
|
+
ingestion: {
|
|
1597
|
+
...diagnostics,
|
|
1598
|
+
importedSessions: sessions.length,
|
|
1599
|
+
cacheTokenAudit: mergeCacheTokenAudits(
|
|
1600
|
+
sessions.map((session) => session.cacheTokenAudit).filter(Boolean),
|
|
1601
|
+
),
|
|
1602
|
+
},
|
|
1603
|
+
sessions,
|
|
1604
|
+
};
|
|
1605
|
+
} finally {
|
|
1606
|
+
diagnostics = previousDiagnostics;
|
|
1607
|
+
DatabaseSync = previousDatabaseSync;
|
|
1608
|
+
usdToEur = previousUsdToEur;
|
|
1609
|
+
scanInProgress = false;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export function writeSessionData(sessionData, outputFile = 'public/data/sessions.json') {
|
|
1614
|
+
const resolvedOutputFile = resolve(outputFile);
|
|
1615
|
+
mkdirSync(dirname(resolvedOutputFile), { recursive: true });
|
|
1616
|
+
writeFileSync(resolvedOutputFile, JSON.stringify(sessionData, null, 2));
|
|
1617
|
+
return resolvedOutputFile;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
export async function runScannerCli(args = process.argv.slice(2), logger = console) {
|
|
1621
|
+
const outputFile = args[0] ?? 'public/data/sessions.json';
|
|
1622
|
+
const roots = args.slice(1);
|
|
1623
|
+
const sessionData = await scanVsCodeSessions(roots.length ? { roots } : {});
|
|
1624
|
+
const resolvedOutputFile = writeSessionData(sessionData, outputFile);
|
|
1625
|
+
|
|
1626
|
+
logger.log(`Wrote ${sessionData.sessions.length} sessions to ${resolvedOutputFile}`);
|
|
1627
|
+
return sessionData;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1631
|
+
await runScannerCli();
|
|
1632
|
+
}
|