copilot-metrics 0.1.3 → 0.1.5
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 +23 -0
- package/README.md +44 -29
- package/package.json +1 -1
- package/src/cli.js +59 -15
- package/src/ingest.js +287 -5
- package/src/jsonl.js +6 -4
- package/src/otel.js +40 -5
- package/src/pricing.js +12 -3
- package/src/reports.js +88 -32
- package/src/setup.js +103 -21
- package/src/sqlite-store.js +199 -0
package/src/reports.js
CHANGED
|
@@ -19,6 +19,10 @@ function formatCredits(value) {
|
|
|
19
19
|
return n(value).toFixed(6);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function usageStatus(row) {
|
|
23
|
+
return n(row.usage_records) > 0 ? 'usage' : 'evidence-only';
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
function table(headers, rows) {
|
|
23
27
|
const widths = headers.map((header, index) => Math.max(
|
|
24
28
|
header.length,
|
|
@@ -48,6 +52,10 @@ SELECT
|
|
|
48
52
|
WHEN (SELECT COUNT(DISTINCT usage_record_id) FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL) = 0 THEN 'hook-only'
|
|
49
53
|
ELSE 'token-bearing'
|
|
50
54
|
END AS token_status,
|
|
55
|
+
CASE
|
|
56
|
+
WHEN (SELECT COUNT(DISTINCT usage_record_id) FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL) = 0 THEN 'evidence-only'
|
|
57
|
+
ELSE 'usage'
|
|
58
|
+
END AS usage_status,
|
|
51
59
|
(SELECT MIN(COALESCE(ur.timestamp, le.timestamp, le.imported_at)) FROM label_evidence le LEFT JOIN usage_records ur ON ur.id = le.usage_record_id WHERE le.label = labels.label) AS first_seen,
|
|
52
60
|
(SELECT MAX(COALESCE(ur.timestamp, le.timestamp, le.imported_at)) FROM label_evidence le LEFT JOIN usage_records ur ON ur.id = le.usage_record_id WHERE le.label = labels.label) AS last_seen,
|
|
53
61
|
(SELECT MAX(estimate_label) FROM usage_records WHERE id IN (SELECT DISTINCT usage_record_id FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL)) AS estimate_label
|
|
@@ -73,6 +81,10 @@ SELECT
|
|
|
73
81
|
WHEN (SELECT COUNT(DISTINCT usage_record_id) FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL) = 0 THEN 'hook-only'
|
|
74
82
|
ELSE 'token-bearing'
|
|
75
83
|
END AS token_status,
|
|
84
|
+
CASE
|
|
85
|
+
WHEN (SELECT COUNT(DISTINCT usage_record_id) FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL) = 0 THEN 'evidence-only'
|
|
86
|
+
ELSE 'usage'
|
|
87
|
+
END AS usage_status,
|
|
76
88
|
(SELECT MIN(COALESCE(ur.timestamp, le.timestamp, le.imported_at)) FROM label_evidence le LEFT JOIN usage_records ur ON ur.id = le.usage_record_id WHERE le.label = labels.label) AS first_seen,
|
|
77
89
|
(SELECT MAX(COALESCE(ur.timestamp, le.timestamp, le.imported_at)) FROM label_evidence le LEFT JOIN usage_records ur ON ur.id = le.usage_record_id WHERE le.label = labels.label) AS last_seen,
|
|
78
90
|
(SELECT MAX(estimate_label) FROM usage_records WHERE id IN (SELECT DISTINCT usage_record_id FROM label_evidence WHERE label = labels.label AND usage_record_id IS NOT NULL)) AS estimate_label
|
|
@@ -80,6 +92,30 @@ FROM (SELECT DISTINCT label FROM label_evidence WHERE label = ?) labels`, [canon
|
|
|
80
92
|
return rows[0] || null;
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
async function labelModelBreakdown(dbPath, label) {
|
|
96
|
+
await initStore(dbPath);
|
|
97
|
+
return queryRows(dbPath, `
|
|
98
|
+
SELECT
|
|
99
|
+
COALESCE(ur.resolved_model, ur.requested_model, 'unknown') AS model,
|
|
100
|
+
COUNT(DISTINCT ur.id) AS usage_records,
|
|
101
|
+
COUNT(DISTINCT ur.session_id) AS sessions,
|
|
102
|
+
SUM(ur.input_tokens) AS input_tokens,
|
|
103
|
+
SUM(ur.output_tokens) AS output_tokens,
|
|
104
|
+
SUM(ur.cache_read_tokens) AS cache_read_tokens,
|
|
105
|
+
SUM(ur.cache_creation_tokens) AS cache_creation_tokens,
|
|
106
|
+
SUM(ur.reasoning_tokens) AS reasoning_tokens,
|
|
107
|
+
SUM(COALESCE(ur.estimated_ai_credits, 0)) AS estimated_ai_credits,
|
|
108
|
+
MAX(ur.estimate_label) AS estimate_label
|
|
109
|
+
FROM usage_records ur
|
|
110
|
+
WHERE ur.id IN (
|
|
111
|
+
SELECT DISTINCT usage_record_id
|
|
112
|
+
FROM label_evidence
|
|
113
|
+
WHERE label = ? AND usage_record_id IS NOT NULL
|
|
114
|
+
)
|
|
115
|
+
GROUP BY COALESCE(ur.resolved_model, ur.requested_model, 'unknown')
|
|
116
|
+
ORDER BY estimated_ai_credits DESC, model`, [canonicalLabel(label)]);
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
async function labelDetails(dbPath, label) {
|
|
84
120
|
await initStore(dbPath);
|
|
85
121
|
return queryRows(dbPath, `
|
|
@@ -173,7 +209,7 @@ ORDER BY ur.timestamp, ur.id`);
|
|
|
173
209
|
function formatLabels(rows) {
|
|
174
210
|
return [
|
|
175
211
|
table(
|
|
176
|
-
['Label', 'Sessions', 'Usage', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'Credits', '
|
|
212
|
+
['Label', 'Sessions', 'Usage', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'AI Credits est.', 'Usage status', 'Evidence', 'Last seen'],
|
|
177
213
|
rows.map((row) => [
|
|
178
214
|
row.label,
|
|
179
215
|
row.sessions,
|
|
@@ -184,39 +220,57 @@ function formatLabels(rows) {
|
|
|
184
220
|
formatNumber(row.cache_creation_tokens),
|
|
185
221
|
formatNumber(row.reasoning_tokens),
|
|
186
222
|
formatCredits(row.estimated_ai_credits),
|
|
187
|
-
row
|
|
223
|
+
usageStatus(row),
|
|
188
224
|
row.evidence_count,
|
|
189
225
|
row.last_seen || '',
|
|
190
226
|
]),
|
|
191
227
|
),
|
|
192
228
|
'',
|
|
193
|
-
`
|
|
229
|
+
`AI Credits are estimates (${estimateLabel(rows)}). 1 AI Credit is treated as $0.01 for local estimates.`,
|
|
194
230
|
].join('\n');
|
|
195
231
|
}
|
|
196
232
|
|
|
197
|
-
function formatLabelSummary(summary
|
|
233
|
+
function formatLabelSummary(summary) {
|
|
198
234
|
if (!summary) return 'No usage found for label.';
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
return table(
|
|
236
|
+
['Label', 'Sessions', 'Usage', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'AI Credits est.', 'Usage status', 'Evidence'],
|
|
237
|
+
[[
|
|
238
|
+
summary.label,
|
|
239
|
+
summary.sessions,
|
|
240
|
+
summary.usage_records,
|
|
241
|
+
formatNumber(summary.input_tokens),
|
|
242
|
+
formatNumber(summary.output_tokens),
|
|
243
|
+
formatNumber(summary.cache_read_tokens),
|
|
244
|
+
formatNumber(summary.cache_creation_tokens),
|
|
245
|
+
formatNumber(summary.reasoning_tokens),
|
|
246
|
+
formatCredits(summary.estimated_ai_credits),
|
|
247
|
+
usageStatus(summary),
|
|
248
|
+
summary.evidence_count,
|
|
249
|
+
]],
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatLabelReport(summary, models, details = null) {
|
|
254
|
+
const output = [formatLabelSummary(summary)];
|
|
255
|
+
if (models && models.length > 0) {
|
|
256
|
+
output.push('', table(
|
|
257
|
+
['Model', 'Sessions', 'Usage', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'AI Credits est.'],
|
|
258
|
+
models.map((row) => [
|
|
259
|
+
row.model,
|
|
260
|
+
row.sessions,
|
|
261
|
+
row.usage_records,
|
|
262
|
+
formatNumber(row.input_tokens),
|
|
263
|
+
formatNumber(row.output_tokens),
|
|
264
|
+
formatNumber(row.cache_read_tokens),
|
|
265
|
+
formatNumber(row.cache_creation_tokens),
|
|
266
|
+
formatNumber(row.reasoning_tokens),
|
|
267
|
+
formatCredits(row.estimated_ai_credits),
|
|
268
|
+
]),
|
|
269
|
+
));
|
|
270
|
+
}
|
|
217
271
|
if (details) {
|
|
218
|
-
|
|
219
|
-
['Source', 'Field', 'Session', 'Model', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'Credits', 'Value'],
|
|
272
|
+
output.push('', table(
|
|
273
|
+
['Source', 'Field', 'Session', 'Model', 'Input', 'Output', 'Cache read', 'Cache create', 'Reasoning', 'AI Credits est.', 'Value'],
|
|
220
274
|
details.map((row) => [
|
|
221
275
|
row.source_type,
|
|
222
276
|
row.source_field,
|
|
@@ -232,52 +286,54 @@ function formatLabelSummary(summary, details = null) {
|
|
|
232
286
|
]),
|
|
233
287
|
));
|
|
234
288
|
}
|
|
235
|
-
|
|
236
|
-
return
|
|
289
|
+
output.push('', `AI Credits are estimates (${summary?.estimate_label || estimateLabel(models || [])}). 1 AI Credit is treated as $0.01 for local estimates.`);
|
|
290
|
+
return output.join('\n');
|
|
237
291
|
}
|
|
238
292
|
|
|
239
293
|
function formatModels(rows) {
|
|
240
294
|
return [
|
|
241
295
|
table(
|
|
242
|
-
['Model', 'Records', 'Input', 'Output', 'Credits'],
|
|
296
|
+
['Model', 'Records', 'Input', 'Output', 'AI Credits est.'],
|
|
243
297
|
rows.map((row) => [row.model, row.usage_records, formatNumber(row.input_tokens), formatNumber(row.output_tokens), formatCredits(row.estimated_ai_credits)]),
|
|
244
298
|
),
|
|
245
299
|
'',
|
|
246
|
-
`
|
|
300
|
+
`AI Credits are estimates (${estimateLabel(rows)}). 1 AI Credit is treated as $0.01 for local estimates.`,
|
|
247
301
|
].join('\n');
|
|
248
302
|
}
|
|
249
303
|
|
|
250
304
|
function formatRepos(rows) {
|
|
251
305
|
return [
|
|
252
306
|
table(
|
|
253
|
-
['Repo', 'CWD', 'Sessions', 'Input', 'Output', 'Credits'],
|
|
307
|
+
['Repo', 'CWD', 'Sessions', 'Input', 'Output', 'AI Credits est.'],
|
|
254
308
|
rows.map((row) => [row.repo, row.cwd, row.sessions, formatNumber(row.input_tokens), formatNumber(row.output_tokens), formatCredits(row.estimated_ai_credits)]),
|
|
255
309
|
),
|
|
256
310
|
'',
|
|
257
|
-
`
|
|
311
|
+
`AI Credits are estimates (${estimateLabel(rows)}). 1 AI Credit is treated as $0.01 for local estimates.`,
|
|
258
312
|
].join('\n');
|
|
259
313
|
}
|
|
260
314
|
|
|
261
315
|
function formatUnattributed(rows) {
|
|
262
316
|
return [
|
|
263
317
|
table(
|
|
264
|
-
['ID', 'Source', 'Session', 'Repo', 'Branch', 'CWD', 'Model', 'Credits'],
|
|
318
|
+
['ID', 'Source', 'Session', 'Repo', 'Branch', 'CWD', 'Model', 'AI Credits est.'],
|
|
265
319
|
rows.map((row) => [row.id, row.source, row.session_id || '', row.repo || '', row.branch || '', row.cwd || '', row.resolved_model || '', formatCredits(row.estimated_ai_credits)]),
|
|
266
320
|
),
|
|
267
321
|
'',
|
|
268
|
-
`
|
|
322
|
+
`AI Credits are estimates (${estimateLabel(rows)}). 1 AI Credit is treated as $0.01 for local estimates.`,
|
|
269
323
|
].join('\n');
|
|
270
324
|
}
|
|
271
325
|
|
|
272
326
|
module.exports = {
|
|
273
327
|
labelOverview,
|
|
274
328
|
labelSummary,
|
|
329
|
+
labelModelBreakdown,
|
|
275
330
|
labelDetails,
|
|
276
331
|
modelReport,
|
|
277
332
|
repoReport,
|
|
278
333
|
unattributedReport,
|
|
279
334
|
formatLabels,
|
|
280
335
|
formatLabelSummary,
|
|
336
|
+
formatLabelReport,
|
|
281
337
|
formatModels,
|
|
282
338
|
formatRepos,
|
|
283
339
|
formatUnattributed,
|
package/src/setup.js
CHANGED
|
@@ -34,6 +34,22 @@ function writePrivateFile(file, data) {
|
|
|
34
34
|
fs.writeFileSync(file, data, { mode: 0o600 });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function readJsonFile(file) {
|
|
38
|
+
if (!fs.existsSync(file)) return {};
|
|
39
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(text);
|
|
42
|
+
} catch {
|
|
43
|
+
return JSON.parse(stripJsonComments(text).replace(/,\s*([}\]])/g, '$1'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripJsonComments(text) {
|
|
48
|
+
return text
|
|
49
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
50
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
function shellQuote(value) {
|
|
38
54
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
39
55
|
}
|
|
@@ -43,29 +59,45 @@ function ensureDataDirs(paths) {
|
|
|
43
59
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
const defaultConfig = {
|
|
63
|
+
version: 1,
|
|
64
|
+
dataHome: paths.home,
|
|
65
|
+
contentCapture: false,
|
|
66
|
+
telemetry: {
|
|
67
|
+
vscode: paths.vscodeOtelJsonl,
|
|
68
|
+
copilotCli: paths.copilotCliOtelJsonl,
|
|
69
|
+
},
|
|
70
|
+
sources: {
|
|
71
|
+
vscode: {
|
|
72
|
+
telemetry: paths.vscodeOtelJsonl,
|
|
73
|
+
hooks: paths.hookEventsJsonl,
|
|
54
74
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
60
|
-
copilotCli: {
|
|
61
|
-
telemetry: paths.copilotCliOtelJsonl,
|
|
62
|
-
hooks: paths.hookEventsJsonl,
|
|
63
|
-
sessions: paths.copilotSessionStateDir,
|
|
64
|
-
},
|
|
75
|
+
copilotCli: {
|
|
76
|
+
telemetry: paths.copilotCliOtelJsonl,
|
|
77
|
+
hooks: paths.hookEventsJsonl,
|
|
78
|
+
sessions: paths.copilotSessionStateDir,
|
|
65
79
|
},
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
},
|
|
81
|
+
labelExtractors: [],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(paths.configJson)) {
|
|
85
|
+
writePrivateFile(paths.configJson, `${JSON.stringify(defaultConfig, null, 2)}\n`);
|
|
86
|
+
return;
|
|
68
87
|
}
|
|
88
|
+
|
|
89
|
+
const current = readJsonFile(paths.configJson);
|
|
90
|
+
const next = {
|
|
91
|
+
...defaultConfig,
|
|
92
|
+
...current,
|
|
93
|
+
telemetry: { ...defaultConfig.telemetry, ...(current.telemetry || {}) },
|
|
94
|
+
sources: {
|
|
95
|
+
vscode: { ...defaultConfig.sources.vscode, ...(current.sources?.vscode || {}) },
|
|
96
|
+
copilotCli: { ...defaultConfig.sources.copilotCli, ...(current.sources?.copilotCli || {}) },
|
|
97
|
+
},
|
|
98
|
+
labelExtractors: current.labelExtractors || defaultConfig.labelExtractors,
|
|
99
|
+
};
|
|
100
|
+
writePrivateFile(paths.configJson, `${JSON.stringify(next, null, 2)}\n`);
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
function vscodeSettings(paths) {
|
|
@@ -77,6 +109,44 @@ function vscodeSettings(paths) {
|
|
|
77
109
|
};
|
|
78
110
|
}
|
|
79
111
|
|
|
112
|
+
function defaultVscodeSettingsTargets(options = {}) {
|
|
113
|
+
const env = options.env || process.env;
|
|
114
|
+
const home = env.HOME || process.env.HOME;
|
|
115
|
+
if (!home) return [];
|
|
116
|
+
if (process.platform === 'darwin') {
|
|
117
|
+
return [
|
|
118
|
+
path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
119
|
+
path.join(home, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json'),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
if (process.platform === 'win32') {
|
|
123
|
+
const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
124
|
+
return [
|
|
125
|
+
path.join(appData, 'Code', 'User', 'settings.json'),
|
|
126
|
+
path.join(appData, 'Code - Insiders', 'User', 'settings.json'),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
return [
|
|
130
|
+
path.join(home, '.config', 'Code', 'User', 'settings.json'),
|
|
131
|
+
path.join(home, '.config', 'Code - Insiders', 'User', 'settings.json'),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function installVscodeSettings(paths, options = {}) {
|
|
136
|
+
const settings = vscodeSettings(paths);
|
|
137
|
+
const explicitTarget = options.target;
|
|
138
|
+
const candidates = explicitTarget ? [explicitTarget] : defaultVscodeSettingsTargets(options);
|
|
139
|
+
const existingTargets = candidates.filter((target) => fs.existsSync(target));
|
|
140
|
+
const targets = existingTargets.length > 0 ? existingTargets : candidates.slice(0, 1);
|
|
141
|
+
const results = [];
|
|
142
|
+
for (const target of targets) {
|
|
143
|
+
const current = readJsonFile(target);
|
|
144
|
+
writePrivateFile(target, `${JSON.stringify({ ...current, ...settings }, null, 2)}\n`);
|
|
145
|
+
results.push({ target, settings });
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
|
|
80
150
|
function copilotCliEnvironment(paths) {
|
|
81
151
|
return {
|
|
82
152
|
COPILOT_OTEL_ENABLED: 'true',
|
|
@@ -110,7 +180,8 @@ function isEphemeralPackageShim(command) {
|
|
|
110
180
|
}
|
|
111
181
|
|
|
112
182
|
function hookEventsForSurface(surface) {
|
|
113
|
-
if (surface === '
|
|
183
|
+
if (surface === 'both') return Array.from(new Set([...COPILOT_CLI_HOOK_EVENTS, ...VSCODE_HOOK_EVENTS]));
|
|
184
|
+
if (surface === 'copilot-cli') return COPILOT_CLI_HOOK_EVENTS;
|
|
114
185
|
if (surface === 'vscode') return VSCODE_HOOK_EVENTS;
|
|
115
186
|
throw new Error(`Unknown hook surface "${surface}". Use "both", "copilot-cli", or "vscode".`);
|
|
116
187
|
}
|
|
@@ -188,11 +259,20 @@ function installHook(paths, options = {}) {
|
|
|
188
259
|
function setupSnapshot(options = {}) {
|
|
189
260
|
const paths = resolvePaths(options);
|
|
190
261
|
ensureDataDirs(paths);
|
|
262
|
+
const vscodeInstalled = options.install === true ? installVscodeSettings(paths, options) : [];
|
|
263
|
+
const hooksInstalled = options.install === true ? installHook(paths, {
|
|
264
|
+
cwd: options.cwd,
|
|
265
|
+
scope: options.scope || 'local',
|
|
266
|
+
surface: options.surface || 'both',
|
|
267
|
+
command: options.command,
|
|
268
|
+
}) : null;
|
|
191
269
|
return {
|
|
192
270
|
paths,
|
|
193
271
|
vscode: vscodeSettings(paths),
|
|
272
|
+
vscodeInstalled,
|
|
194
273
|
copilotCli: copilotCliEnvironment(paths),
|
|
195
274
|
hooks: hookConfig(paths, options),
|
|
275
|
+
hooksInstalled,
|
|
196
276
|
};
|
|
197
277
|
}
|
|
198
278
|
|
|
@@ -201,7 +281,9 @@ module.exports = {
|
|
|
201
281
|
COPILOT_CLI_HOOK_EVENTS,
|
|
202
282
|
VSCODE_HOOK_EVENTS,
|
|
203
283
|
ensureDataDirs,
|
|
284
|
+
defaultVscodeSettingsTargets,
|
|
204
285
|
vscodeSettings,
|
|
286
|
+
installVscodeSettings,
|
|
205
287
|
copilotCliEnvironment,
|
|
206
288
|
shellExports,
|
|
207
289
|
shellQuote,
|
package/src/sqlite-store.js
CHANGED
|
@@ -141,6 +141,7 @@ function lastInsertId(db) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function insertLabelEvidence(db, importedAt, evidenceRows) {
|
|
144
|
+
if (!evidenceRows.length) return;
|
|
144
145
|
runPrepared(
|
|
145
146
|
db,
|
|
146
147
|
`INSERT INTO label_evidence (
|
|
@@ -183,6 +184,16 @@ async function existingRawFingerprints(dbPath, source, sourceFile, fingerprints)
|
|
|
183
184
|
return existing;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
async function importedLineHighWater(dbPath, source, sourceFile) {
|
|
188
|
+
await initStore(dbPath);
|
|
189
|
+
const rows = await queryRows(
|
|
190
|
+
dbPath,
|
|
191
|
+
'SELECT COALESCE(MAX(line), 0) AS line FROM raw_records WHERE source = ? AND source_file = ?',
|
|
192
|
+
[source, sourceFile],
|
|
193
|
+
);
|
|
194
|
+
return Number(rows[0]?.line || 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
async function insertImport(dbPath, source, sourceFile, rawRecords, usageRecords, hookEvents, warnings) {
|
|
187
198
|
await initStore(dbPath);
|
|
188
199
|
const db = await openDatabase(dbPath);
|
|
@@ -273,6 +284,7 @@ async function insertImport(dbPath, source, sourceFile, rawRecords, usageRecords
|
|
|
273
284
|
repo: event.repo,
|
|
274
285
|
branch: event.branch,
|
|
275
286
|
cwd: event.cwd,
|
|
287
|
+
timestamp: event.timestamp,
|
|
276
288
|
});
|
|
277
289
|
}
|
|
278
290
|
}
|
|
@@ -297,6 +309,188 @@ async function insertImport(dbPath, source, sourceFile, rawRecords, usageRecords
|
|
|
297
309
|
persistDatabase(dbPath, db);
|
|
298
310
|
}
|
|
299
311
|
|
|
312
|
+
async function attachVscodeChatLabelEvidence(dbPath, mappings) {
|
|
313
|
+
await initStore(dbPath);
|
|
314
|
+
if (!mappings.length) {
|
|
315
|
+
return { matched_usage_records: 0, label_evidence: 0 };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const db = await openDatabase(dbPath);
|
|
319
|
+
const importedAt = new Date().toISOString();
|
|
320
|
+
let matchedUsageRecords = 0;
|
|
321
|
+
let labelEvidence = 0;
|
|
322
|
+
|
|
323
|
+
const usageStatement = db.prepare(`
|
|
324
|
+
SELECT id, session_id, repo, branch, cwd, timestamp
|
|
325
|
+
FROM usage_records
|
|
326
|
+
WHERE source = 'vscode' AND span_id = ?
|
|
327
|
+
`);
|
|
328
|
+
const existingStatement = db.prepare(`
|
|
329
|
+
SELECT 1
|
|
330
|
+
FROM label_evidence
|
|
331
|
+
WHERE label = ?
|
|
332
|
+
AND source_type = 'usage'
|
|
333
|
+
AND source_field = 'vscode_chat_response'
|
|
334
|
+
AND source_value = ?
|
|
335
|
+
AND usage_record_id = ?
|
|
336
|
+
LIMIT 1
|
|
337
|
+
`);
|
|
338
|
+
const insertStatement = db.prepare(`
|
|
339
|
+
INSERT INTO label_evidence (
|
|
340
|
+
imported_at, label, source_type, source_field, source_value, confidence,
|
|
341
|
+
usage_record_id, hook_event_id, session_id, repo, branch, cwd, timestamp
|
|
342
|
+
) VALUES (?, ?, 'usage', 'vscode_chat_response', ?, ?, ?, NULL, ?, ?, ?, ?, ?)
|
|
343
|
+
`);
|
|
344
|
+
|
|
345
|
+
db.run('BEGIN');
|
|
346
|
+
try {
|
|
347
|
+
for (const mapping of mappings) {
|
|
348
|
+
usageStatement.bind([mapping.responseId]);
|
|
349
|
+
const usageRows = [];
|
|
350
|
+
while (usageStatement.step()) usageRows.push(usageStatement.getAsObject());
|
|
351
|
+
usageStatement.reset();
|
|
352
|
+
matchedUsageRecords += usageRows.length;
|
|
353
|
+
|
|
354
|
+
for (const usage of usageRows) {
|
|
355
|
+
for (const evidence of mapping.label_evidence || []) {
|
|
356
|
+
existingStatement.bind([evidence.label, mapping.responseId, usage.id]);
|
|
357
|
+
const exists = existingStatement.step();
|
|
358
|
+
existingStatement.reset();
|
|
359
|
+
if (exists) continue;
|
|
360
|
+
|
|
361
|
+
insertStatement.run([
|
|
362
|
+
importedAt,
|
|
363
|
+
evidence.label,
|
|
364
|
+
mapping.responseId,
|
|
365
|
+
evidence.confidence || 0.95,
|
|
366
|
+
usage.id,
|
|
367
|
+
mapping.sessionId || usage.session_id || null,
|
|
368
|
+
usage.repo || null,
|
|
369
|
+
usage.branch || null,
|
|
370
|
+
usage.cwd || null,
|
|
371
|
+
usage.timestamp || null,
|
|
372
|
+
]);
|
|
373
|
+
labelEvidence += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
db.run('COMMIT');
|
|
378
|
+
} catch (error) {
|
|
379
|
+
db.run('ROLLBACK');
|
|
380
|
+
throw error;
|
|
381
|
+
} finally {
|
|
382
|
+
usageStatement.free();
|
|
383
|
+
existingStatement.free();
|
|
384
|
+
insertStatement.free();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
persistDatabase(dbPath, db);
|
|
388
|
+
return { matched_usage_records: matchedUsageRecords, label_evidence: labelEvidence };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function vscodeRawRecordsNeedingResponseBackfill(dbPath, sourceFile) {
|
|
392
|
+
await initStore(dbPath);
|
|
393
|
+
return queryRows(dbPath, `
|
|
394
|
+
SELECT rr.line, rr.payload_json
|
|
395
|
+
FROM raw_records rr
|
|
396
|
+
WHERE rr.source = 'vscode'
|
|
397
|
+
AND rr.source_file = ?
|
|
398
|
+
AND EXISTS (
|
|
399
|
+
SELECT 1
|
|
400
|
+
FROM usage_records ur
|
|
401
|
+
WHERE ur.source = 'vscode'
|
|
402
|
+
AND ur.raw_line = rr.line
|
|
403
|
+
AND ur.span_id IS NULL
|
|
404
|
+
)
|
|
405
|
+
`, [sourceFile]);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function updateVscodeUsageResponseIds(dbPath, updates) {
|
|
409
|
+
await initStore(dbPath);
|
|
410
|
+
if (!updates.length) return 0;
|
|
411
|
+
|
|
412
|
+
const db = await openDatabase(dbPath);
|
|
413
|
+
const statement = db.prepare(`
|
|
414
|
+
UPDATE usage_records
|
|
415
|
+
SET span_id = ?,
|
|
416
|
+
session_id = COALESCE(session_id, ?),
|
|
417
|
+
timestamp = COALESCE(timestamp, ?)
|
|
418
|
+
WHERE source = 'vscode'
|
|
419
|
+
AND raw_line = ?
|
|
420
|
+
AND span_id IS NULL
|
|
421
|
+
AND input_tokens = ?
|
|
422
|
+
AND output_tokens = ?
|
|
423
|
+
AND cache_read_tokens = ?
|
|
424
|
+
AND cache_creation_tokens = ?
|
|
425
|
+
AND reasoning_tokens = ?
|
|
426
|
+
AND COALESCE(resolved_model, requested_model, '') = COALESCE(?, '')
|
|
427
|
+
`);
|
|
428
|
+
let updated = 0;
|
|
429
|
+
db.run('BEGIN');
|
|
430
|
+
try {
|
|
431
|
+
for (const update of updates) {
|
|
432
|
+
statement.run([
|
|
433
|
+
update.span_id,
|
|
434
|
+
update.session_id || null,
|
|
435
|
+
update.timestamp || null,
|
|
436
|
+
update.raw_line,
|
|
437
|
+
update.input_tokens,
|
|
438
|
+
update.output_tokens,
|
|
439
|
+
update.cache_read_tokens,
|
|
440
|
+
update.cache_creation_tokens,
|
|
441
|
+
update.reasoning_tokens,
|
|
442
|
+
update.resolved_model || update.requested_model || '',
|
|
443
|
+
]);
|
|
444
|
+
updated += typeof db.getRowsModified === 'function' ? db.getRowsModified() : 0;
|
|
445
|
+
}
|
|
446
|
+
db.run('COMMIT');
|
|
447
|
+
} catch (error) {
|
|
448
|
+
db.run('ROLLBACK');
|
|
449
|
+
throw error;
|
|
450
|
+
} finally {
|
|
451
|
+
statement.free();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
persistDatabase(dbPath, db);
|
|
455
|
+
return updated;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function updateUsageCostEstimates(dbPath, updates) {
|
|
459
|
+
await initStore(dbPath);
|
|
460
|
+
if (!updates.length) return 0;
|
|
461
|
+
|
|
462
|
+
const db = await openDatabase(dbPath);
|
|
463
|
+
const statement = db.prepare(`
|
|
464
|
+
UPDATE usage_records
|
|
465
|
+
SET estimated_usd = ?,
|
|
466
|
+
estimated_ai_credits = ?,
|
|
467
|
+
warnings_json = ?
|
|
468
|
+
WHERE id = ?
|
|
469
|
+
`);
|
|
470
|
+
let updated = 0;
|
|
471
|
+
db.run('BEGIN');
|
|
472
|
+
try {
|
|
473
|
+
for (const update of updates) {
|
|
474
|
+
statement.run([
|
|
475
|
+
update.estimated_usd,
|
|
476
|
+
update.estimated_ai_credits,
|
|
477
|
+
JSON.stringify(update.warnings || []),
|
|
478
|
+
update.id,
|
|
479
|
+
]);
|
|
480
|
+
updated += typeof db.getRowsModified === 'function' ? db.getRowsModified() : 0;
|
|
481
|
+
}
|
|
482
|
+
db.run('COMMIT');
|
|
483
|
+
} catch (error) {
|
|
484
|
+
db.run('ROLLBACK');
|
|
485
|
+
throw error;
|
|
486
|
+
} finally {
|
|
487
|
+
statement.free();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
persistDatabase(dbPath, db);
|
|
491
|
+
return updated;
|
|
492
|
+
}
|
|
493
|
+
|
|
300
494
|
async function queryOne(dbPath, sql) {
|
|
301
495
|
const db = await openDatabase(dbPath);
|
|
302
496
|
const result = db.exec(sql);
|
|
@@ -319,9 +513,14 @@ async function queryRows(dbPath, sql, params = []) {
|
|
|
319
513
|
}
|
|
320
514
|
|
|
321
515
|
module.exports = {
|
|
516
|
+
attachVscodeChatLabelEvidence,
|
|
322
517
|
existingRawFingerprints,
|
|
518
|
+
importedLineHighWater,
|
|
323
519
|
initStore,
|
|
324
520
|
insertImport,
|
|
325
521
|
queryOne,
|
|
326
522
|
queryRows,
|
|
523
|
+
updateUsageCostEstimates,
|
|
524
|
+
updateVscodeUsageResponseIds,
|
|
525
|
+
vscodeRawRecordsNeedingResponseBackfill,
|
|
327
526
|
};
|