copilot-usage-studio 0.1.0 → 0.2.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -4
  2. package/README.md +206 -150
  3. package/data/github-copilot-pricing.json +34 -6
  4. package/dist/copilot-usage-studio/browser/chunk-2SAR2Q6M.js +1 -0
  5. package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
  6. package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +1 -0
  7. package/dist/copilot-usage-studio/browser/chunk-EWN3YOHT.js +1 -0
  8. package/dist/copilot-usage-studio/browser/chunk-KS5W2HMH.js +1 -0
  9. package/dist/copilot-usage-studio/browser/chunk-OS2XPCVW.js +2 -0
  10. package/dist/copilot-usage-studio/browser/chunk-U4H425LY.js +1 -0
  11. package/dist/copilot-usage-studio/browser/chunk-VEKXIM47.js +1 -0
  12. package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
  13. package/dist/copilot-usage-studio/browser/chunk-XHID4XVE.js +1 -0
  14. package/dist/copilot-usage-studio/browser/index.html +11 -11
  15. package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +1 -0
  16. package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
  17. package/docs/copilot-memory.md +91 -0
  18. package/docs/local-deployment.md +119 -13
  19. package/docs/pricing.md +3 -3
  20. package/docs/scanner-api.md +32 -3
  21. package/lib/cli.mjs +15 -1
  22. package/lib/local-runtime.mjs +530 -19
  23. package/lib/scanner-worker.mjs +25 -0
  24. package/package.json +95 -78
  25. package/scripts/pricing-utils.mjs +5 -0
  26. package/scripts/scan-vscode-sessions.mjs +418 -1204
  27. package/scripts/scanner-customization-evidence.mjs +576 -0
  28. package/scripts/scanner-customization-inventory.mjs +1021 -0
  29. package/scripts/scanner-memory.mjs +229 -0
  30. package/scripts/scanner-session-parser.mjs +1237 -0
  31. package/scripts/scanner-traversal.mjs +203 -0
  32. package/scripts/scanner-workspace.mjs +209 -0
  33. package/dist/copilot-usage-studio/browser/chunk-C6VWIY5S.js +0 -1
  34. package/dist/copilot-usage-studio/browser/chunk-DLWQO3VR.js +0 -1
  35. package/dist/copilot-usage-studio/browser/chunk-F6TIG2GE.js +0 -4
  36. package/dist/copilot-usage-studio/browser/chunk-JIP7ONRZ.js +0 -1
  37. package/dist/copilot-usage-studio/browser/chunk-RNKEPBEU.js +0 -1
  38. package/dist/copilot-usage-studio/browser/chunk-Z3XIAKMM.js +0 -1
  39. package/dist/copilot-usage-studio/browser/main-C6XOJRSH.js +0 -2
  40. package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +0 -1
@@ -0,0 +1,1237 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { basename, isAbsolute, join, resolve, sep } from 'node:path';
3
+ import {
4
+ costBreakdownUsdForTokens,
5
+ costUsdForTokens,
6
+ modelKey,
7
+ normalizeModel,
8
+ pricingModelForModel,
9
+ } from './pricing-utils.mjs';
10
+
11
+ export function createSessionParser(context = {}) {
12
+ const pricing = context.pricing ?? {};
13
+ const fallbackPricingModel = context.fallbackPricingModel ?? '';
14
+ const traceEventLimit = context.traceEventLimit ?? 1000;
15
+ const diagnostics = () => context.diagnostics?.() ?? context.diagnostics ?? { warnings: [] };
16
+ const usdToEur = () => Number(context.usdToEur?.() ?? context.usdToEur ?? 1);
17
+ const readJsonl = (...args) => context.readJsonl?.(...args) ?? [];
18
+ const safeJson = (...args) => context.safeJson?.(...args) ?? null;
19
+ const listFiles = (...args) => context.listFiles?.(...args) ?? [];
20
+ const memoryRecallsFromDebugLog = (...args) => context.memoryRecallsFromDebugLog?.(...args) ?? [];
21
+ const workspaceName = (...args) => context.workspaceName?.(...args) ?? '';
22
+
23
+ function costUsd(model, tokens) {
24
+ return costUsdForTokens(model, tokens, pricing, fallbackPricingModel);
25
+ }
26
+
27
+ function costBreakdownUsd(model, tokens) {
28
+ return costBreakdownUsdForTokens(model, tokens, pricing, fallbackPricingModel);
29
+ }
30
+
31
+ function transcriptAvailability(workspaceDir, sessionId) {
32
+ const sourcePath = join(workspaceDir, 'GitHub.copilot-chat', 'transcripts', `${sessionId}.jsonl`);
33
+
34
+ if (!existsSync(sourcePath)) {
35
+ return {
36
+ available: false,
37
+ sourcePath: '',
38
+ eventCount: 0,
39
+ };
40
+ }
41
+
42
+ const eventCount = readJsonl(sourcePath).length;
43
+
44
+ return {
45
+ available: true,
46
+ sourcePath,
47
+ eventCount,
48
+ };
49
+ }
50
+
51
+ function numericAttr(attrs, names) {
52
+ for (const name of names) {
53
+ const value = Number(attrs?.[name] ?? 0);
54
+ if (Number.isFinite(value) && value > 0) {
55
+ return value;
56
+ }
57
+ }
58
+
59
+ return 0;
60
+ }
61
+
62
+ function llmTokenFields(event) {
63
+ const inputTokens = Number(event.attrs?.inputTokens ?? 0);
64
+ const outputTokens = Number(event.attrs?.outputTokens ?? 0);
65
+ const rawCachedInputTokens = numericAttr(event.attrs, [
66
+ 'cachedTokens',
67
+ 'cachedInputTokens',
68
+ 'cacheReadTokens',
69
+ ]);
70
+ const cachedInputTokens = Math.min(inputTokens, rawCachedInputTokens);
71
+ const cacheWriteTokens = numericAttr(event.attrs, ['cacheWriteTokens', 'cachedWriteTokens']);
72
+ const billableInputTokens = Math.max(0, inputTokens - cachedInputTokens);
73
+
74
+ return {
75
+ inputTokens,
76
+ billableInputTokens,
77
+ rawCachedInputTokens,
78
+ cachedInputTokens,
79
+ cacheWriteTokens,
80
+ outputTokens,
81
+ };
82
+ }
83
+
84
+ function cacheTokenAuditFromLlmRequests(llmRequests) {
85
+ return llmRequests.reduce(
86
+ (audit, event) => {
87
+ const tokenFields = llmTokenFields(event);
88
+
89
+ audit.modelCalls += 1;
90
+ audit.rawInputTokens += tokenFields.inputTokens;
91
+ audit.normalInputTokens += tokenFields.billableInputTokens;
92
+ audit.cachedInputTokens += tokenFields.cachedInputTokens;
93
+ audit.cacheWriteTokens += tokenFields.cacheWriteTokens;
94
+ audit.outputTokens += tokenFields.outputTokens;
95
+
96
+ if (tokenFields.rawCachedInputTokens > 0) {
97
+ audit.callsWithCachedTokens += 1;
98
+ }
99
+
100
+ if (tokenFields.rawCachedInputTokens > tokenFields.inputTokens) {
101
+ audit.invalidCachedTokenSplits += 1;
102
+ }
103
+
104
+ const rawInputShare =
105
+ tokenFields.inputTokens > 0 ? tokenFields.cachedInputTokens / tokenFields.inputTokens : 0;
106
+ audit.maxCachedInputShare = Math.max(audit.maxCachedInputShare, rawInputShare);
107
+
108
+ return audit;
109
+ },
110
+ {
111
+ modelCalls: 0,
112
+ callsWithCachedTokens: 0,
113
+ invalidCachedTokenSplits: 0,
114
+ rawInputTokens: 0,
115
+ normalInputTokens: 0,
116
+ cachedInputTokens: 0,
117
+ cacheWriteTokens: 0,
118
+ outputTokens: 0,
119
+ maxCachedInputShare: 0,
120
+ },
121
+ );
122
+ }
123
+
124
+ function mergeCacheTokenAudits(audits) {
125
+ return audits.reduce(
126
+ (total, audit) => ({
127
+ modelCalls: total.modelCalls + audit.modelCalls,
128
+ callsWithCachedTokens: total.callsWithCachedTokens + audit.callsWithCachedTokens,
129
+ invalidCachedTokenSplits: total.invalidCachedTokenSplits + audit.invalidCachedTokenSplits,
130
+ rawInputTokens: total.rawInputTokens + audit.rawInputTokens,
131
+ normalInputTokens: total.normalInputTokens + audit.normalInputTokens,
132
+ cachedInputTokens: total.cachedInputTokens + audit.cachedInputTokens,
133
+ cacheWriteTokens: total.cacheWriteTokens + audit.cacheWriteTokens,
134
+ outputTokens: total.outputTokens + audit.outputTokens,
135
+ maxCachedInputShare: Math.max(total.maxCachedInputShare, audit.maxCachedInputShare),
136
+ }),
137
+ {
138
+ modelCalls: 0,
139
+ callsWithCachedTokens: 0,
140
+ invalidCachedTokenSplits: 0,
141
+ rawInputTokens: 0,
142
+ normalInputTokens: 0,
143
+ cachedInputTokens: 0,
144
+ cacheWriteTokens: 0,
145
+ outputTokens: 0,
146
+ maxCachedInputShare: 0,
147
+ },
148
+ );
149
+ }
150
+
151
+ function eventModelCostFields(rawModel, tokenFields) {
152
+ const normalizedModel = normalizeModel(rawModel, pricing);
153
+ const pricingModel = pricingModelForModel(normalizedModel, pricing, fallbackPricingModel);
154
+ const tokens = {
155
+ input: tokenFields.billableInputTokens,
156
+ cachedInput: tokenFields.cachedInputTokens,
157
+ cacheWrite: tokenFields.cacheWriteTokens,
158
+ output: tokenFields.outputTokens,
159
+ };
160
+ const costBreakdown = costBreakdownUsd(pricingModel, tokens);
161
+
162
+ return {
163
+ model: normalizedModel,
164
+ rawModel:
165
+ String(rawModel ?? '')
166
+ .replace(/^copilot\//i, '')
167
+ .trim() || 'unknown',
168
+ pricingModel,
169
+ pricingTier: costBreakdown.tier,
170
+ totalTokens: tokenFields.inputTokens + tokenFields.outputTokens + tokenFields.cacheWriteTokens,
171
+ estimatedCost: { usd: costBreakdown.total, eur: costBreakdown.total * usdToEur() },
172
+ };
173
+ }
174
+
175
+ function parseMaybeJson(value) {
176
+ if (typeof value !== 'string') {
177
+ return value ?? null;
178
+ }
179
+
180
+ return safeJson(value) ?? value;
181
+ }
182
+
183
+ function charLength(value) {
184
+ if (value === undefined || value === null) {
185
+ return 0;
186
+ }
187
+
188
+ return typeof value === 'string' ? value.length : JSON.stringify(value).length;
189
+ }
190
+
191
+ function parseContentFile(file) {
192
+ const envelope = safeJson(readFileSync(file, 'utf8'));
193
+ const content = parseMaybeJson(envelope?.content ?? envelope);
194
+
195
+ return {
196
+ file,
197
+ content,
198
+ chars: charLength(content),
199
+ };
200
+ }
201
+
202
+ function modelCapabilityIndex(sessionDir) {
203
+ const file = join(sessionDir, 'models.json');
204
+ if (!existsSync(file)) {
205
+ return new Map();
206
+ }
207
+
208
+ const parsed = safeJson(readFileSync(file, 'utf8'));
209
+ const models = Array.isArray(parsed) ? parsed : [];
210
+ const index = new Map();
211
+
212
+ for (const model of models) {
213
+ const keys = [model?.id, model?.name, model?.version, model?.capabilities?.family]
214
+ .filter(Boolean)
215
+ .map(modelKey);
216
+
217
+ for (const key of keys) {
218
+ if (key) {
219
+ index.set(key, model);
220
+ }
221
+ }
222
+ }
223
+
224
+ return index;
225
+ }
226
+
227
+ function modelCapabilityFor(rawModel, capabilityIndex) {
228
+ const key = modelKey(rawModel);
229
+
230
+ return (
231
+ capabilityIndex.get(key) ??
232
+ [...capabilityIndex.entries()].find(([candidate]) => key.includes(candidate))?.[1] ??
233
+ null
234
+ );
235
+ }
236
+
237
+ function modelLimitSummaries(sessionDir, llmRequests) {
238
+ const capabilityIndex = modelCapabilityIndex(sessionDir);
239
+ if (!capabilityIndex.size || !llmRequests.length) {
240
+ return [];
241
+ }
242
+
243
+ const byModel = new Map();
244
+
245
+ for (const event of llmRequests) {
246
+ const rawModel = String(event.attrs?.model ?? '')
247
+ .replace(/^copilot\//i, '')
248
+ .trim();
249
+ const displayModel = normalizeModel(rawModel, pricing);
250
+ const capability = modelCapabilityFor(rawModel || displayModel, capabilityIndex);
251
+ const limits = capability?.capabilities?.limits ?? {};
252
+ const supports = capability?.capabilities?.supports ?? {};
253
+ const current = byModel.get(displayModel) ?? {
254
+ model: displayModel,
255
+ rawModels: new Set(),
256
+ modelId: capability?.id ?? rawModel,
257
+ vendor: capability?.vendor ?? '',
258
+ tokenizer: capability?.capabilities?.tokenizer ?? '',
259
+ contextWindowTokens: Number(limits.max_context_window_tokens ?? 0) || 0,
260
+ promptLimitTokens: Number(limits.max_prompt_tokens ?? 0) || 0,
261
+ outputLimitTokens: Number(limits.max_output_tokens ?? 0) || 0,
262
+ supportedReasoningEfforts: Array.isArray(supports.reasoning_effort)
263
+ ? supports.reasoning_effort
264
+ : [],
265
+ supportedEndpoints: Array.isArray(capability?.supported_endpoints)
266
+ ? capability.supported_endpoints
267
+ : [],
268
+ modelPickerEnabled: Boolean(capability?.model_picker_enabled),
269
+ isChatDefault: Boolean(capability?.is_chat_default),
270
+ isChatFallback: Boolean(capability?.is_chat_fallback),
271
+ modelCalls: 0,
272
+ largestRawInputTokens: 0,
273
+ totalRawInputTokens: 0,
274
+ largestOutputTokens: 0,
275
+ };
276
+
277
+ current.rawModels.add(rawModel || 'unknown');
278
+ current.modelCalls += 1;
279
+ current.largestRawInputTokens = Math.max(
280
+ current.largestRawInputTokens,
281
+ Number(event.attrs?.inputTokens ?? 0),
282
+ );
283
+ current.totalRawInputTokens += Number(event.attrs?.inputTokens ?? 0);
284
+ current.largestOutputTokens = Math.max(
285
+ current.largestOutputTokens,
286
+ Number(event.attrs?.outputTokens ?? 0),
287
+ );
288
+ byModel.set(displayModel, current);
289
+ }
290
+
291
+ return [...byModel.values()].map((summary) => ({
292
+ ...summary,
293
+ rawModels: [...summary.rawModels],
294
+ promptLimitShare:
295
+ summary.promptLimitTokens > 0
296
+ ? summary.largestRawInputTokens / summary.promptLimitTokens
297
+ : null,
298
+ contextWindowShare:
299
+ summary.contextWindowTokens > 0
300
+ ? summary.largestRawInputTokens / summary.contextWindowTokens
301
+ : null,
302
+ repeatedInputFactor:
303
+ summary.largestRawInputTokens > 0
304
+ ? summary.totalRawInputTokens / summary.largestRawInputTokens
305
+ : 0,
306
+ }));
307
+ }
308
+
309
+ function toolName(tool) {
310
+ return String(tool?.function?.name ?? tool?.name ?? tool?.toolName ?? tool?.id ?? 'unknown_tool');
311
+ }
312
+
313
+ function toolSchemaSize(tool) {
314
+ const descriptionChars = charLength(tool?.function?.description ?? tool?.description);
315
+ const parameterChars = charLength(
316
+ tool?.function?.parameters ?? tool?.parameters ?? tool?.input_schema,
317
+ );
318
+
319
+ return {
320
+ name: toolName(tool),
321
+ descriptionChars,
322
+ parameterChars,
323
+ totalChars: charLength(tool),
324
+ };
325
+ }
326
+
327
+ function requestOptions(event) {
328
+ const parsed = parseMaybeJson(event.attrs?.requestOptions);
329
+ return parsed && typeof parsed === 'object' ? parsed : {};
330
+ }
331
+
332
+ function reasoningEffort(event) {
333
+ return String(requestOptions(event)?.reasoning?.effort ?? '').trim();
334
+ }
335
+
336
+ function textVerbosity(event) {
337
+ return String(requestOptions(event)?.text?.verbosity ?? '').trim();
338
+ }
339
+
340
+ function requestShapeMetadata(event) {
341
+ const shape = parseMaybeJson(event.attrs?.requestShape);
342
+
343
+ if (!shape || typeof shape !== 'object') {
344
+ return null;
345
+ }
346
+
347
+ return {
348
+ api: shape.api ? String(shape.api) : '',
349
+ inputItemCount: Number(shape.inputItemCount ?? 0),
350
+ inputItemTypes: Array.isArray(shape.inputItemTypes)
351
+ ? shape.inputItemTypes.filter(Boolean).map(String)
352
+ : [],
353
+ hasPreviousResponseId: Boolean(shape.hasPreviousResponseId),
354
+ };
355
+ }
356
+
357
+ function requestShapeSummary(event) {
358
+ const shape = requestShapeMetadata(event);
359
+ if (!shape) {
360
+ return '';
361
+ }
362
+
363
+ const parts = [
364
+ shape.api ? `api: ${shape.api}` : '',
365
+ shape.inputItemCount
366
+ ? `${shape.inputItemCount.toLocaleString()} input item${shape.inputItemCount === 1 ? '' : 's'}`
367
+ : '',
368
+ shape.inputItemTypes.length ? `types: ${shape.inputItemTypes.join(', ')}` : '',
369
+ shape.hasPreviousResponseId ? 'continues previous response' : '',
370
+ ].filter(Boolean);
371
+
372
+ return parts.join(' · ');
373
+ }
374
+
375
+ function countedValues(values) {
376
+ const counts = new Map();
377
+
378
+ for (const value of values.filter(Boolean)) {
379
+ counts.set(value, (counts.get(value) ?? 0) + 1);
380
+ }
381
+
382
+ return [...counts.entries()]
383
+ .map(([value, count]) => ({ value, count }))
384
+ .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
385
+ }
386
+
387
+ function toolPayloadSummary(toolEvents) {
388
+ const byName = new Map();
389
+
390
+ for (const event of toolEvents) {
391
+ const name = String(
392
+ event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type ?? 'tool',
393
+ );
394
+ const current = byName.get(name) ?? { name, calls: 0, argsChars: 0, resultChars: 0 };
395
+ current.calls += 1;
396
+ current.argsChars += charLength(
397
+ event.attrs?.args ??
398
+ event.attrs?.arguments ??
399
+ event.attrs?.input ??
400
+ event.data?.args ??
401
+ event.data?.arguments ??
402
+ event.data?.input,
403
+ );
404
+ current.resultChars += charLength(
405
+ event.attrs?.result ??
406
+ event.attrs?.output ??
407
+ event.attrs?.stdout ??
408
+ event.data?.result ??
409
+ event.data?.output ??
410
+ event.data?.stdout,
411
+ );
412
+ byName.set(name, current);
413
+ }
414
+
415
+ return [...byName.values()]
416
+ .sort(
417
+ (a, b) =>
418
+ b.resultChars + b.argsChars - (a.resultChars + a.argsChars) || a.name.localeCompare(b.name),
419
+ )
420
+ .slice(0, 12);
421
+ }
422
+
423
+ function sessionSideFilePath(sessionDir, file) {
424
+ const value = String(file ?? '').trim();
425
+ if (!value || isAbsolute(value)) {
426
+ return '';
427
+ }
428
+
429
+ const root = `${resolve(sessionDir)}${sep}`;
430
+ const candidate = resolve(sessionDir, value);
431
+ return candidate.startsWith(root) ? candidate : '';
432
+ }
433
+
434
+ function requestPayloadSummary(sessionDir, llmRequests, toolEvents) {
435
+ const systemPromptFiles = [
436
+ ...new Set(llmRequests.map((event) => event.attrs?.systemPromptFile).filter(Boolean)),
437
+ ];
438
+ const toolsFiles = [
439
+ ...new Set(llmRequests.map((event) => event.attrs?.toolsFile).filter(Boolean)),
440
+ ];
441
+ const systemPrompts = systemPromptFiles
442
+ .map((file) => sessionSideFilePath(sessionDir, file))
443
+ .filter(Boolean)
444
+ .filter(existsSync)
445
+ .map(parseContentFile);
446
+ const toolFileSummaries = toolsFiles
447
+ .map((file) => sessionSideFilePath(sessionDir, file))
448
+ .filter(Boolean)
449
+ .filter(existsSync)
450
+ .map(parseContentFile);
451
+ const tools = toolFileSummaries.flatMap((summary) =>
452
+ Array.isArray(summary.content) ? summary.content : [],
453
+ );
454
+ const toolSchemas = tools.map(toolSchemaSize);
455
+ const mcpToolNames = toolSchemas
456
+ .map((tool) => tool.name)
457
+ .filter((name) => name.startsWith('mcp_'));
458
+ const reasoningEfforts = countedValues(llmRequests.map(reasoningEffort)).map(
459
+ ({ value, count }) => ({
460
+ effort: value,
461
+ count,
462
+ }),
463
+ );
464
+ const subagentLogCount = listFiles(sessionDir, '.jsonl').filter((file) =>
465
+ basename(file).startsWith('runSubagent-'),
466
+ ).length;
467
+
468
+ return {
469
+ systemPromptFiles: systemPrompts.length,
470
+ systemPromptChars: systemPrompts.reduce((sum, summary) => sum + summary.chars, 0),
471
+ toolSchemaFiles: toolFileSummaries.length,
472
+ toolSchemaChars: toolFileSummaries.reduce((sum, summary) => sum + summary.chars, 0),
473
+ toolCount: toolSchemas.length,
474
+ mcpToolCount: mcpToolNames.length,
475
+ mcpToolNames: [...new Set(mcpToolNames)].sort(),
476
+ largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 8),
477
+ modelCallsWithSystemPromptFile: llmRequests.filter((event) => event.attrs?.systemPromptFile)
478
+ .length,
479
+ modelCallsWithToolsFile: llmRequests.filter((event) => event.attrs?.toolsFile).length,
480
+ reasoningEfforts,
481
+ toolResultCharsByName: toolPayloadSummary(toolEvents),
482
+ subagentLogCount,
483
+ };
484
+ }
485
+
486
+ function contentSummaryFromCache(sessionDir, cache, file) {
487
+ if (!file) {
488
+ return null;
489
+ }
490
+
491
+ if (cache.has(file)) {
492
+ return cache.get(file);
493
+ }
494
+
495
+ const path = sessionSideFilePath(sessionDir, file);
496
+ const summary = existsSync(path) ? parseContentFile(path) : null;
497
+ cache.set(file, summary);
498
+ return summary;
499
+ }
500
+
501
+ function modelCallSetupPayloadFactory(sessionDir) {
502
+ const cache = new Map();
503
+
504
+ return (event) => {
505
+ if (event.type !== 'llm_request') {
506
+ return null;
507
+ }
508
+
509
+ const systemPromptFile = String(event.attrs?.systemPromptFile ?? '').trim();
510
+ const toolsFile = String(event.attrs?.toolsFile ?? '').trim();
511
+
512
+ if (!systemPromptFile && !toolsFile) {
513
+ return null;
514
+ }
515
+
516
+ const systemPrompt = contentSummaryFromCache(sessionDir, cache, systemPromptFile);
517
+ const toolsSummary = contentSummaryFromCache(sessionDir, cache, toolsFile);
518
+ const tools = Array.isArray(toolsSummary?.content) ? toolsSummary.content : [];
519
+ const toolSchemas = tools.map(toolSchemaSize);
520
+ const mcpToolNames = toolSchemas
521
+ .map((tool) => tool.name)
522
+ .filter((name) => name.startsWith('mcp_'));
523
+
524
+ return {
525
+ systemPromptFile,
526
+ systemPromptChars: systemPrompt?.chars ?? 0,
527
+ toolsFile,
528
+ toolSchemaChars: toolsSummary?.chars ?? 0,
529
+ toolCount: toolSchemas.length,
530
+ mcpToolCount: mcpToolNames.length,
531
+ mcpToolNames: [...new Set(mcpToolNames)].sort(),
532
+ largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 5),
533
+ };
534
+ };
535
+ }
536
+
537
+ function debugEvidence(llmRequests, agentResponses) {
538
+ const inputSeries = llmRequests.map((event) => Number(event.attrs?.inputTokens ?? 0));
539
+ const outputCaps = [
540
+ ...new Set(
541
+ llmRequests.map((event) => Number(event.attrs?.maxTokens ?? 0)).filter((value) => value > 0),
542
+ ),
543
+ ].sort((a, b) => a - b);
544
+ const maxInputTokens = Math.max(0, ...inputSeries);
545
+ const maxRequestTokens = Math.max(0, ...outputCaps);
546
+ const reasoningEvents = agentResponses.filter((event) =>
547
+ String(event.attrs?.reasoning ?? '').trim(),
548
+ ).length;
549
+ const efforts = countedValues(llmRequests.map(reasoningEffort));
550
+ const primaryEffort = efforts[0]?.value ?? '';
551
+
552
+ return {
553
+ reasoning: {
554
+ visible: reasoningEvents > 0 || Boolean(primaryEffort),
555
+ level: primaryEffort,
556
+ events: reasoningEvents,
557
+ source: primaryEffort
558
+ ? 'llm_request.attrs.requestOptions.reasoning.effort'
559
+ : reasoningEvents > 0
560
+ ? 'agent_response.attrs.reasoning'
561
+ : '',
562
+ help: primaryEffort
563
+ ? 'VS Code Agent Debug Logs expose the request reasoning effort in llm_request.attrs.requestOptions.reasoning.effort.'
564
+ : reasoningEvents > 0
565
+ ? 'VS Code debug logs include reasoning text on agent_response events, but no request reasoning effort was imported.'
566
+ : 'No reasoning text field was present on imported agent_response events.',
567
+ },
568
+ context: {
569
+ maxInputTokens,
570
+ maxRequestTokens,
571
+ outputCaps,
572
+ requestCapShare: maxRequestTokens > 0 ? maxInputTokens / maxRequestTokens : null,
573
+ source:
574
+ maxRequestTokens > 0
575
+ ? 'llm_request.attrs.inputTokens and attrs.maxTokens'
576
+ : 'llm_request.attrs.inputTokens',
577
+ help:
578
+ maxRequestTokens > 0
579
+ ? '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.'
580
+ : 'Largest observed model input token count. The log did not include a request cap to compare against.',
581
+ },
582
+ };
583
+ }
584
+
585
+ function modelBreakdownFromLlmRequests(llmRequests) {
586
+ const byModel = new Map();
587
+
588
+ for (const event of llmRequests) {
589
+ const rawModel = String(event.attrs?.model ?? 'unknown')
590
+ .replace(/^copilot\//i, '')
591
+ .trim();
592
+ const displayModel = normalizeModel(rawModel, pricing);
593
+ const current = byModel.get(displayModel) ?? {
594
+ model: displayModel,
595
+ rawModels: new Set(),
596
+ turns: 0,
597
+ tokens: { input: 0, cachedInput: 0, cacheWrite: 0, output: 0 },
598
+ cost: { usd: 0, eur: 0 },
599
+ costBreakdown: { inputUsd: 0, cachedInputUsd: 0, cacheWriteUsd: 0, outputUsd: 0 },
600
+ pricingTiers: new Set(),
601
+ pricingModel: pricingModelForModel(displayModel, pricing, fallbackPricingModel),
602
+ };
603
+
604
+ current.rawModels.add(rawModel || 'unknown');
605
+ current.turns += 1;
606
+ const tokenFields = llmTokenFields(event);
607
+ current.tokens.input += tokenFields.billableInputTokens;
608
+ current.tokens.cachedInput += tokenFields.cachedInputTokens;
609
+ current.tokens.cacheWrite += tokenFields.cacheWriteTokens;
610
+ current.tokens.output += tokenFields.outputTokens;
611
+ const callCost = costBreakdownUsd(current.pricingModel, {
612
+ input: tokenFields.billableInputTokens,
613
+ cachedInput: tokenFields.cachedInputTokens,
614
+ cacheWrite: tokenFields.cacheWriteTokens,
615
+ output: tokenFields.outputTokens,
616
+ });
617
+ current.costBreakdown.inputUsd += callCost.input;
618
+ current.costBreakdown.cachedInputUsd += callCost.cachedInput;
619
+ current.costBreakdown.cacheWriteUsd += callCost.cacheWrite;
620
+ current.costBreakdown.outputUsd += callCost.output;
621
+ current.pricingTiers.add(callCost.tier);
622
+ byModel.set(displayModel, current);
623
+ }
624
+
625
+ return [...byModel.values()].map((entry) => {
626
+ const usd =
627
+ entry.costBreakdown.inputUsd +
628
+ entry.costBreakdown.cachedInputUsd +
629
+ entry.costBreakdown.cacheWriteUsd +
630
+ entry.costBreakdown.outputUsd;
631
+ return {
632
+ ...entry,
633
+ rawModels: [...entry.rawModels],
634
+ pricingTiers: [...entry.pricingTiers],
635
+ cost: { usd, eur: usd * usdToEur() },
636
+ };
637
+ });
638
+ }
639
+
640
+ function sessionModelLabel(modelBreakdown, fallbackModel) {
641
+ if (modelBreakdown.length === 1) {
642
+ return modelBreakdown[0].model;
643
+ }
644
+
645
+ if (modelBreakdown.length > 1) {
646
+ return `Mixed (${modelBreakdown.map((entry) => entry.model).join(', ')})`;
647
+ }
648
+
649
+ return normalizeModel(fallbackModel, pricing);
650
+ }
651
+
652
+ function estimateTokens(text) {
653
+ const compact = String(text ?? '').trim();
654
+ if (!compact) {
655
+ return 0;
656
+ }
657
+
658
+ return Math.max(1, Math.round(Math.max(compact.split(/\s+/).length * 1.35, compact.length / 4)));
659
+ }
660
+
661
+ function timestampForEvent(event) {
662
+ return event?.timestamp ?? new Date(Number(event?.ts ?? 0)).toISOString();
663
+ }
664
+
665
+ function eventDetail(event) {
666
+ if (event.type === 'llm_request') {
667
+ return `${event.attrs?.model ?? 'model'}: ${Number(event.attrs?.inputTokens ?? 0).toLocaleString()} raw in / ${Number(
668
+ event.attrs?.outputTokens ?? 0,
669
+ ).toLocaleString()} out`;
670
+ }
671
+
672
+ if (String(event.type ?? '').includes('tool')) {
673
+ return String(event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type);
674
+ }
675
+
676
+ if (event.type === 'user_message') {
677
+ return String(event.attrs?.content ?? '').slice(0, 140);
678
+ }
679
+
680
+ if (event.type === 'agent_response') {
681
+ return parseAssistantResponse(event.attrs?.response).slice(0, 140);
682
+ }
683
+
684
+ return String(event.attrs?.details ?? event.name ?? event.type ?? '').slice(0, 140);
685
+ }
686
+
687
+ function boundedText(value, maxLength = 260) {
688
+ const compact = String(value ?? '')
689
+ .replace(/\s+/g, ' ')
690
+ .trim();
691
+ return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
692
+ }
693
+
694
+ function summaryValue(value) {
695
+ if (value === undefined || value === null || value === '') {
696
+ return '';
697
+ }
698
+
699
+ if (typeof value === 'string') {
700
+ return boundedText(value);
701
+ }
702
+
703
+ if (typeof value === 'number' || typeof value === 'boolean') {
704
+ return String(value);
705
+ }
706
+
707
+ return boundedText(JSON.stringify(compactObject(value)));
708
+ }
709
+
710
+ function sourceEstimatedCost(event) {
711
+ return summaryValue(event.attrs?.estimatedCost);
712
+ }
713
+
714
+ function sourceUsageFromNanoAiu(event) {
715
+ const nanoAiu = Number(event.attrs?.copilotUsageNanoAiu ?? 0);
716
+ if (!Number.isFinite(nanoAiu) || nanoAiu <= 0) {
717
+ return null;
718
+ }
719
+
720
+ const credits = nanoAiu / 1_000_000_000;
721
+
722
+ return {
723
+ nanoAiu,
724
+ credits,
725
+ usd: credits * 0.01,
726
+ modelCalls: 1,
727
+ };
728
+ }
729
+
730
+ function sourceUsageSummary(llmRequests) {
731
+ const usages = llmRequests.map(sourceUsageFromNanoAiu).filter(Boolean);
732
+ const nanoAiu = usages.reduce((sum, usage) => sum + usage.nanoAiu, 0);
733
+ const credits = nanoAiu / 1_000_000_000;
734
+
735
+ return {
736
+ nanoAiu,
737
+ credits,
738
+ usd: credits * 0.01,
739
+ modelCalls: usages.length,
740
+ };
741
+ }
742
+
743
+ function compactObject(value) {
744
+ if (!value || typeof value !== 'object') {
745
+ return value;
746
+ }
747
+
748
+ const result = {};
749
+ const skip = new Set(['content', 'response', 'result', 'output', 'stdout', 'stderr']);
750
+
751
+ for (const [key, nestedValue] of Object.entries(value)) {
752
+ if (skip.has(key)) {
753
+ continue;
754
+ }
755
+
756
+ if (nestedValue === undefined || nestedValue === null) {
757
+ continue;
758
+ }
759
+
760
+ result[key] =
761
+ typeof nestedValue === 'object'
762
+ ? boundedText(JSON.stringify(nestedValue), 120)
763
+ : boundedText(nestedValue, 120);
764
+
765
+ if (Object.keys(result).length >= 6) {
766
+ break;
767
+ }
768
+ }
769
+
770
+ return result;
771
+ }
772
+
773
+ function eventAttributeSummary(event) {
774
+ const attrs = event.attrs ?? {};
775
+ const data = event.data ?? {};
776
+ const candidates = [
777
+ ['category', attrs.category],
778
+ ['source', attrs.source],
779
+ ['model', attrs.model],
780
+ ['debugName', attrs.debugName],
781
+ ['inputTokens', attrs.inputTokens],
782
+ ['cachedTokens', attrs.cachedTokens ?? attrs.cachedInputTokens ?? attrs.cacheReadTokens],
783
+ ['cacheWriteTokens', attrs.cacheWriteTokens ?? attrs.cachedWriteTokens],
784
+ ['outputTokens', attrs.outputTokens],
785
+ ['sourceEstimatedCost', attrs.estimatedCost],
786
+ ['copilotUsageNanoAiu', attrs.copilotUsageNanoAiu],
787
+ ['reasoningEffort', event.type === 'llm_request' ? reasoningEffort(event) : undefined],
788
+ ['textVerbosity', event.type === 'llm_request' ? textVerbosity(event) : undefined],
789
+ ['requestShape', event.type === 'llm_request' ? requestShapeSummary(event) : undefined],
790
+ ['maxTokens', attrs.maxTokens],
791
+ ['ttft', attrs.ttft],
792
+ ['systemPromptFile', attrs.systemPromptFile],
793
+ ['toolsFile', attrs.toolsFile],
794
+ ['vscodeVersion', attrs.vscodeVersion],
795
+ ['copilotVersion', attrs.copilotVersion],
796
+ ['responseId', attrs.responseId],
797
+ ['logVersion', event.v],
798
+ ['toolName', data.toolName ?? attrs.toolName],
799
+ ['details', attrs.details],
800
+ ['content', attrs.content],
801
+ [
802
+ 'response',
803
+ event.type === 'agent_response' ? parseAssistantResponse(attrs.response) : undefined,
804
+ ],
805
+ ['data', data && Object.keys(data).length ? data : undefined],
806
+ ];
807
+
808
+ const fields = [];
809
+ const seen = new Set();
810
+
811
+ for (const [label, value] of candidates) {
812
+ const summarized = summaryValue(value);
813
+
814
+ if (!summarized || seen.has(label)) {
815
+ continue;
816
+ }
817
+
818
+ fields.push({ label, value: summarized });
819
+ seen.add(label);
820
+
821
+ if (fields.length >= 8) {
822
+ break;
823
+ }
824
+ }
825
+
826
+ return fields;
827
+ }
828
+
829
+ function capTraceEvents(events) {
830
+ return events.slice(0, traceEventLimit);
831
+ }
832
+
833
+ function parseAssistantResponse(raw) {
834
+ const parsed = typeof raw === 'string' ? safeJson(raw) : raw;
835
+ if (!Array.isArray(parsed)) {
836
+ return String(raw ?? '');
837
+ }
838
+
839
+ return parsed
840
+ .flatMap((message) => message?.parts ?? [])
841
+ .map((part) => {
842
+ const content =
843
+ typeof part?.content === 'string'
844
+ ? (safeJson(part.content) ?? part.content)
845
+ : part?.content;
846
+ return typeof content === 'object' && content?.text ? content.text : String(content ?? '');
847
+ })
848
+ .filter(Boolean)
849
+ .join('\n');
850
+ }
851
+
852
+ function sessionFromDebugLog(sessionDir, workspaceDir) {
853
+ const main = readJsonl(join(sessionDir, 'main.jsonl'));
854
+ if (!main.length) {
855
+ diagnostics().skippedEmptyDebugLogs += 1;
856
+ return null;
857
+ }
858
+
859
+ const sid = basename(sessionDir);
860
+ const userMessages = main.filter((event) => event.type === 'user_message');
861
+ const llmRequests = main.filter((event) => event.type === 'llm_request');
862
+ const assistantEvents = main.filter(
863
+ (event) => event.type === 'agent_response' || event.type === 'assistant.message',
864
+ );
865
+ const toolEvents = main.filter((event) => String(event.type ?? '').includes('tool'));
866
+ const errorEvents = main.filter((event) => event.status && event.status !== 'ok');
867
+
868
+ if (!userMessages.length && !llmRequests.length && !assistantEvents.length) {
869
+ diagnostics().skippedEmptyDebugLogs += 1;
870
+ return null;
871
+ }
872
+
873
+ const firstUserMessage = userMessages[0]?.attrs?.content ?? 'Untitled Copilot session';
874
+ const modelBreakdown = modelBreakdownFromLlmRequests(llmRequests);
875
+ const model = sessionModelLabel(
876
+ modelBreakdown,
877
+ llmRequests.find((event) => event.attrs?.model)?.attrs?.model,
878
+ );
879
+ const input = llmRequests.reduce(
880
+ (sum, event) => sum + llmTokenFields(event).billableInputTokens,
881
+ 0,
882
+ );
883
+ const cachedInput = llmRequests.reduce(
884
+ (sum, event) => sum + llmTokenFields(event).cachedInputTokens,
885
+ 0,
886
+ );
887
+ const cacheWrite = llmRequests.reduce(
888
+ (sum, event) => sum + llmTokenFields(event).cacheWriteTokens,
889
+ 0,
890
+ );
891
+ const output = llmRequests.reduce((sum, event) => sum + llmTokenFields(event).outputTokens, 0);
892
+ const tokens = { input, cachedInput, cacheWrite, output };
893
+ const usd = modelBreakdown.length
894
+ ? modelBreakdown.reduce((sum, entry) => sum + entry.cost.usd, 0)
895
+ : costUsd(model, tokens);
896
+ const startEvent = main.find((event) => event.type === 'session_start') ?? main[0];
897
+ const debugLogRuntime = {
898
+ logVersion: Number(startEvent?.v ?? 0) || 0,
899
+ vscodeVersion: String(startEvent?.attrs?.vscodeVersion ?? '').trim(),
900
+ copilotVersion: String(startEvent?.attrs?.copilotVersion ?? '').trim(),
901
+ };
902
+ const lastEvent = main[main.length - 1];
903
+ const startedAt =
904
+ startEvent?.timestamp ??
905
+ new Date(
906
+ Number(startEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
907
+ ).toISOString();
908
+ const endedAt =
909
+ lastEvent?.timestamp ??
910
+ new Date(
911
+ Number(lastEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
912
+ ).toISOString();
913
+ const evidence = debugEvidence(llmRequests, assistantEvents, main);
914
+ const payload = requestPayloadSummary(sessionDir, llmRequests, toolEvents);
915
+ const modelLimits = modelLimitSummaries(sessionDir, llmRequests);
916
+ const cacheTokenAudit = cacheTokenAuditFromLlmRequests(llmRequests);
917
+ const sourceUsage = sourceUsageSummary(llmRequests);
918
+ const setupPayloadForEvent = modelCallSetupPayloadFactory(sessionDir);
919
+ const transcript = transcriptAvailability(workspaceDir, sid);
920
+ const memoryRecalls = memoryRecallsFromDebugLog(sessionDir, workspaceName(workspaceDir));
921
+
922
+ if (transcript.available) {
923
+ diagnostics().debugLogSessionsWithTranscripts += 1;
924
+ diagnostics().transcriptEventsAvailable += transcript.eventCount;
925
+ }
926
+
927
+ if (cacheTokenAudit.invalidCachedTokenSplits > 0) {
928
+ diagnostics().warnings.push(
929
+ `${sessionDir}: ${cacheTokenAudit.invalidCachedTokenSplits} model call(s) reported cachedTokens greater than inputTokens; cached input was clamped for pricing safety`,
930
+ );
931
+ }
932
+
933
+ const turns = [
934
+ ...userMessages.map((event) => ({
935
+ role: 'user',
936
+ text: String(event.attrs?.content ?? ''),
937
+ tokens: estimateTokens(event.attrs?.content),
938
+ })),
939
+ ...assistantEvents.map((event) => {
940
+ const text =
941
+ event.type === 'assistant.message'
942
+ ? event.data?.content
943
+ : parseAssistantResponse(event.attrs?.response);
944
+ return { role: 'assistant', text: String(text ?? ''), tokens: estimateTokens(text) };
945
+ }),
946
+ ].filter((turn) => turn.text.trim());
947
+
948
+ return {
949
+ id: sid,
950
+ sourceKind: 'vscode-copilot-debug-log',
951
+ tokenSource: llmRequests.length
952
+ ? 'llm_request_token_totals'
953
+ : 'debug-log-visible-text-estimate',
954
+ sessionType: 'Local',
955
+ location: 'Chat Panel',
956
+ status: 'Idle',
957
+ title: firstUserMessage.slice(0, 80),
958
+ firstPrompt: firstUserMessage.slice(0, 240),
959
+ workspace: workspaceName(workspaceDir),
960
+ sourcePath: sessionDir,
961
+ ...(debugLogRuntime.logVersion ||
962
+ debugLogRuntime.vscodeVersion ||
963
+ debugLogRuntime.copilotVersion
964
+ ? { debugLogRuntime }
965
+ : {}),
966
+ model,
967
+ modelBreakdown,
968
+ startedAt,
969
+ endedAt,
970
+ tags: ['debug-log', llmRequests.length ? 'llm-request-token-totals' : 'estimated-visible-text'],
971
+ toolsUsed: [
972
+ ...new Set(
973
+ toolEvents
974
+ .map((event) => event.data?.toolName ?? event.attrs?.toolName ?? event.name)
975
+ .filter(Boolean),
976
+ ),
977
+ ],
978
+ tokens,
979
+ cost: { usd, eur: usd * usdToEur() },
980
+ confidence: llmRequests.length ? 'exact' : 'estimated',
981
+ traceSummary: {
982
+ modelTurns: llmRequests.length,
983
+ toolCalls: toolEvents.length,
984
+ totalTokens: input + cachedInput + cacheWrite + output,
985
+ errors: errorEvents.length,
986
+ totalEvents: main.length,
987
+ reasoningEvents: evidence.reasoning.events,
988
+ maxInputTokens: evidence.context.maxInputTokens,
989
+ maxRequestTokens: evidence.context.maxRequestTokens,
990
+ reasoningEfforts: payload.reasoningEfforts,
991
+ },
992
+ cacheTokenAudit,
993
+ ...(sourceUsage.modelCalls > 0 ? { sourceUsage } : {}),
994
+ transcript,
995
+ advancedSignals: evidence,
996
+ requestPayload: payload,
997
+ modelLimits,
998
+ ...(memoryRecalls.length ? { memoryRecalls } : {}),
999
+ traceEvents: capTraceEvents(
1000
+ main.map((event, index) => {
1001
+ const tokenFields =
1002
+ event.type === 'llm_request'
1003
+ ? llmTokenFields(event)
1004
+ : {
1005
+ inputTokens: 0,
1006
+ billableInputTokens: 0,
1007
+ cachedInputTokens: 0,
1008
+ cacheWriteTokens: 0,
1009
+ outputTokens: 0,
1010
+ };
1011
+
1012
+ const setupPayload = setupPayloadForEvent(event);
1013
+ const sourceUsage = sourceUsageFromNanoAiu(event);
1014
+ const requestShape = event.type === 'llm_request' ? requestShapeMetadata(event) : null;
1015
+
1016
+ return {
1017
+ index,
1018
+ timestamp: timestampForEvent(event),
1019
+ type: String(event.type ?? 'unknown'),
1020
+ name: String(event.name ?? event.type ?? 'unknown'),
1021
+ status: String(event.status ?? 'unknown'),
1022
+ detail: eventDetail(event),
1023
+ attributes: eventAttributeSummary(event),
1024
+ inputTokens: tokenFields.inputTokens,
1025
+ cachedInputTokens: tokenFields.cachedInputTokens,
1026
+ cacheWriteTokens: tokenFields.cacheWriteTokens,
1027
+ outputTokens: tokenFields.outputTokens,
1028
+ ttftMs: event.type === 'llm_request' ? Number(event.attrs?.ttft ?? 0) : 0,
1029
+ maxTokens: event.type === 'llm_request' ? Number(event.attrs?.maxTokens ?? 0) : 0,
1030
+ hasReasoning:
1031
+ event.type === 'agent_response' && Boolean(String(event.attrs?.reasoning ?? '').trim()),
1032
+ reasoningEffort: event.type === 'llm_request' ? reasoningEffort(event) : '',
1033
+ ...(requestShape ? { requestShape } : {}),
1034
+ ...(event.type === 'llm_request'
1035
+ ? eventModelCostFields(event.attrs?.model, tokenFields)
1036
+ : {}),
1037
+ ...(event.type === 'llm_request' && sourceEstimatedCost(event)
1038
+ ? { sourceEstimatedCost: sourceEstimatedCost(event) }
1039
+ : {}),
1040
+ ...(sourceUsage ? { sourceUsage } : {}),
1041
+ ...(setupPayload ? { setupPayload } : {}),
1042
+ };
1043
+ }),
1044
+ ),
1045
+ turns: turns.slice(0, 60),
1046
+ };
1047
+ }
1048
+
1049
+ function sessionFromChatSnapshot(file, workspaceDir) {
1050
+ const records = readJsonl(file);
1051
+ const snapshot =
1052
+ records.find((record) => record.kind === 0 && record.v?.requests)?.v ??
1053
+ records[0]?.v ??
1054
+ records[0];
1055
+ const requests = snapshot?.requests ?? [];
1056
+
1057
+ if (!requests.length) {
1058
+ diagnostics().skippedChatSnapshotsWithoutRequests += 1;
1059
+ return null;
1060
+ }
1061
+
1062
+ const firstRequest = requests[0];
1063
+ const firstPrompt =
1064
+ firstRequest?.message?.text ?? snapshot?.customTitle ?? 'Untitled Copilot session';
1065
+ const model = normalizeModel(
1066
+ firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier,
1067
+ pricing,
1068
+ );
1069
+ const output = requests.reduce((sum, request) => sum + Number(request.completionTokens ?? 0), 0);
1070
+ const input = requests.reduce(
1071
+ (sum, request) => sum + estimateTokens(request?.message?.text ?? ''),
1072
+ 0,
1073
+ );
1074
+ const tokens = { input, cachedInput: 0, cacheWrite: 0, output };
1075
+ const usd = costUsd(model, tokens);
1076
+ const pricingModel = pricingModelForModel(model, pricing, fallbackPricingModel);
1077
+ const startedAt = new Date(Number(snapshot.creationDate ?? statSync(file).mtimeMs)).toISOString();
1078
+ const endedAt = statSync(file).mtime.toISOString();
1079
+
1080
+ return {
1081
+ id: basename(file, '.jsonl'),
1082
+ sourceKind: 'vscode-chat-session-snapshot',
1083
+ tokenSource: 'chat-snapshot-output-plus-visible-input-estimate',
1084
+ sessionType: 'Local',
1085
+ location: 'Chat Panel',
1086
+ status: 'Idle',
1087
+ title: String(snapshot.customTitle ?? firstPrompt).slice(0, 80),
1088
+ firstPrompt: String(firstPrompt).slice(0, 240),
1089
+ workspace: workspaceName(workspaceDir),
1090
+ sourcePath: file,
1091
+ model,
1092
+ modelBreakdown: [
1093
+ {
1094
+ model,
1095
+ rawModels: [
1096
+ String(
1097
+ firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier ?? model,
1098
+ ),
1099
+ ],
1100
+ turns: requests.length,
1101
+ tokens,
1102
+ cost: { usd, eur: usd * usdToEur() },
1103
+ pricingModel,
1104
+ },
1105
+ ],
1106
+ startedAt,
1107
+ endedAt,
1108
+ tags: ['chat-session', 'estimated-input'],
1109
+ toolsUsed: [],
1110
+ tokens,
1111
+ cost: { usd, eur: usd * usdToEur() },
1112
+ confidence: 'estimated',
1113
+ traceSummary: {
1114
+ modelTurns: requests.length,
1115
+ toolCalls: 0,
1116
+ totalTokens: input + output,
1117
+ errors: 0,
1118
+ totalEvents: records.length,
1119
+ reasoningEvents: 0,
1120
+ maxInputTokens: input,
1121
+ maxRequestTokens: 0,
1122
+ },
1123
+ cacheTokenAudit: {
1124
+ modelCalls: 0,
1125
+ callsWithCachedTokens: 0,
1126
+ invalidCachedTokenSplits: 0,
1127
+ rawInputTokens: 0,
1128
+ normalInputTokens: 0,
1129
+ cachedInputTokens: 0,
1130
+ cacheWriteTokens: 0,
1131
+ outputTokens: 0,
1132
+ maxCachedInputShare: 0,
1133
+ },
1134
+ transcript: {
1135
+ available: false,
1136
+ sourcePath: '',
1137
+ eventCount: 0,
1138
+ },
1139
+ advancedSignals: {
1140
+ reasoning: {
1141
+ visible: false,
1142
+ level: '',
1143
+ events: 0,
1144
+ source: '',
1145
+ help: 'Chat snapshots do not expose agent_response reasoning text or a reasoning-level field.',
1146
+ },
1147
+ context: {
1148
+ maxInputTokens: input,
1149
+ maxRequestTokens: 0,
1150
+ outputCaps: [],
1151
+ requestCapShare: null,
1152
+ source: 'estimated visible chat text',
1153
+ help: 'Chat snapshots only provide visible text context here, so context pressure is not reliable for cost debugging.',
1154
+ },
1155
+ },
1156
+ traceEvents: capTraceEvents(
1157
+ requests.flatMap((request, index) => {
1158
+ const rawRequestModel =
1159
+ request?.modelId ?? request?.inputState?.selectedModel?.identifier ?? model;
1160
+ const userInputTokens = estimateTokens(request?.message?.text ?? '');
1161
+ const assistantOutputTokens = Number(request.completionTokens ?? 0);
1162
+
1163
+ return [
1164
+ {
1165
+ index: index * 2,
1166
+ timestamp: startedAt,
1167
+ type: 'user_message',
1168
+ name: 'user_message',
1169
+ status: 'ok',
1170
+ detail: String(request?.message?.text ?? '').slice(0, 140),
1171
+ inputTokens: userInputTokens,
1172
+ cachedInputTokens: 0,
1173
+ cacheWriteTokens: 0,
1174
+ outputTokens: 0,
1175
+ ...eventModelCostFields(rawRequestModel, {
1176
+ inputTokens: userInputTokens,
1177
+ billableInputTokens: userInputTokens,
1178
+ cachedInputTokens: 0,
1179
+ cacheWriteTokens: 0,
1180
+ outputTokens: 0,
1181
+ }),
1182
+ },
1183
+ {
1184
+ index: index * 2 + 1,
1185
+ timestamp: endedAt,
1186
+ type: 'assistant_response',
1187
+ name: 'assistant_response',
1188
+ status: 'ok',
1189
+ detail: `${assistantOutputTokens.toLocaleString()} completion tokens`,
1190
+ inputTokens: 0,
1191
+ cachedInputTokens: 0,
1192
+ cacheWriteTokens: 0,
1193
+ outputTokens: assistantOutputTokens,
1194
+ ...eventModelCostFields(rawRequestModel, {
1195
+ inputTokens: 0,
1196
+ billableInputTokens: 0,
1197
+ cachedInputTokens: 0,
1198
+ cacheWriteTokens: 0,
1199
+ outputTokens: assistantOutputTokens,
1200
+ }),
1201
+ },
1202
+ ];
1203
+ }),
1204
+ ),
1205
+ turns: requests
1206
+ .flatMap((request) => [
1207
+ {
1208
+ role: 'user',
1209
+ text: String(request?.message?.text ?? ''),
1210
+ tokens: estimateTokens(request?.message?.text),
1211
+ },
1212
+ {
1213
+ role: 'assistant',
1214
+ text: (request?.response ?? [])
1215
+ .map((part) => part.value ?? part.generatedTitle ?? part.kind ?? '')
1216
+ .filter(Boolean)
1217
+ .join('\n'),
1218
+ tokens: Number(request.completionTokens ?? 0),
1219
+ },
1220
+ ])
1221
+ .filter((turn) => turn.text.trim())
1222
+ .slice(0, 60),
1223
+ };
1224
+ }
1225
+
1226
+
1227
+ return {
1228
+ cacheTokenAuditFromLlmRequests,
1229
+ eventModelCostFields,
1230
+ llmTokenFields,
1231
+ mergeCacheTokenAudits,
1232
+ modelBreakdownFromLlmRequests,
1233
+ sessionFromChatSnapshot,
1234
+ sessionFromDebugLog,
1235
+ timestampForEvent,
1236
+ };
1237
+ }