copilot-tracer 1.0.6 → 1.0.7
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/dist/consoleUi.js +2 -2
- package/dist/otlpReceiver.js +56 -2
- package/dist/setup.js +8 -7
- package/package.json +1 -1
- package/web/index.html +8 -8
package/dist/consoleUi.js
CHANGED
|
@@ -10,7 +10,7 @@ function fmtDuration(ms) {
|
|
|
10
10
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
11
11
|
}
|
|
12
12
|
function fmtCredits(c) {
|
|
13
|
-
return
|
|
13
|
+
return `$${(c * 0.01).toFixed(4)}`;
|
|
14
14
|
}
|
|
15
15
|
function statusColor(status, text) {
|
|
16
16
|
if (status === 'running')
|
|
@@ -25,7 +25,7 @@ export function renderConsoleTable(entries, summary) {
|
|
|
25
25
|
head: [
|
|
26
26
|
chalk.cyan('Date / Time'),
|
|
27
27
|
chalk.cyan('Prompt'),
|
|
28
|
-
chalk.cyan('
|
|
28
|
+
chalk.cyan('Est Cost'),
|
|
29
29
|
chalk.cyan('Duration'),
|
|
30
30
|
chalk.cyan('Tokens\nCached|Written|Reason'),
|
|
31
31
|
chalk.cyan('Skills'),
|
package/dist/otlpReceiver.js
CHANGED
|
@@ -59,6 +59,31 @@ function getBodyText(body) {
|
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
61
61
|
const inFlight = new Map(); // traceId → InFlight
|
|
62
|
+
// Anthropic API pricing per 1K tokens (USD), converted to GitHub "AI credit" units
|
|
63
|
+
// (1 credit = $0.01) so aiCredits stays one unit across Copilot and Claude entries.
|
|
64
|
+
// Rates are Anthropic's current published per-model prices; a model id that doesn't
|
|
65
|
+
// match a specific entry falls back to its tier's (opus/sonnet/haiku) latest rate.
|
|
66
|
+
const ANTHROPIC_USD_PER_1K = {
|
|
67
|
+
'claude-fable-5': { input: 0.010, output: 0.050 },
|
|
68
|
+
'claude-mythos-5': { input: 0.010, output: 0.050 },
|
|
69
|
+
'claude-opus-5': { input: 0.005, output: 0.025 },
|
|
70
|
+
'claude-opus-4-8': { input: 0.005, output: 0.025 },
|
|
71
|
+
'claude-opus-4-7': { input: 0.005, output: 0.025 },
|
|
72
|
+
'claude-opus-4-6': { input: 0.005, output: 0.025 },
|
|
73
|
+
'claude-sonnet-5': { input: 0.002, output: 0.010 },
|
|
74
|
+
'claude-sonnet-4-6': { input: 0.003, output: 0.015 },
|
|
75
|
+
'claude-haiku-4-5': { input: 0.001, output: 0.005 },
|
|
76
|
+
'opus': { input: 0.005, output: 0.025 },
|
|
77
|
+
'sonnet': { input: 0.003, output: 0.015 },
|
|
78
|
+
'haiku': { input: 0.001, output: 0.005 },
|
|
79
|
+
'default': { input: 0.003, output: 0.015 },
|
|
80
|
+
};
|
|
81
|
+
export function calcClaudeCredits(tokens, model) {
|
|
82
|
+
const key = Object.keys(ANTHROPIC_USD_PER_1K).find(k => (model ?? '').toLowerCase().includes(k)) ?? 'default';
|
|
83
|
+
const rate = ANTHROPIC_USD_PER_1K[key];
|
|
84
|
+
const usd = (tokens.input / 1000) * rate.input + (tokens.output / 1000) * rate.output;
|
|
85
|
+
return usd * 100; // USD → credits (1 credit = $0.01)
|
|
86
|
+
}
|
|
62
87
|
// Sessions are created lazily. Always upsert so project_id gets backfilled
|
|
63
88
|
// when the session was created earlier without a project.
|
|
64
89
|
function ensureSession(sessionId, projectId) {
|
|
@@ -89,6 +114,10 @@ const pendingChatIds = new Map(); // `chat:${traceId}` → list of entryIds
|
|
|
89
114
|
const pendingToolCalls = new Map(); // traceId → tool calls
|
|
90
115
|
const claudePromptEntries = new Map(); // prompt.id → trace entry
|
|
91
116
|
const claudeInteractionEntries = new Map(); // traceId → interaction entry
|
|
117
|
+
// Real Claude Code telemetry sends the child claude_code.llm_request span before its
|
|
118
|
+
// parent claude_code.interaction span within the same batch, so buffer llm_request's
|
|
119
|
+
// token/cost delta here until the interaction entry shows up to receive it.
|
|
120
|
+
const pendingClaudeLlmDeltas = new Map();
|
|
92
121
|
const CLAUDE_STATE_LIMIT = 1000;
|
|
93
122
|
function rememberClaudeEntry(map, key, entry) {
|
|
94
123
|
map.set(key, entry);
|
|
@@ -211,13 +240,14 @@ function processSpans(spans, sessionId, projectId, workingDir) {
|
|
|
211
240
|
const attrs = span.attributes ?? [];
|
|
212
241
|
const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
|
|
213
242
|
const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
|
|
243
|
+
const model = getStringAttr(attrs, 'model', 'gen_ai.request.model');
|
|
214
244
|
const entry = {
|
|
215
245
|
id: spanId,
|
|
216
246
|
sessionId: getStringAttr(attrs, 'session.id') ?? sessionId,
|
|
217
247
|
dateTime: nanoToIso(span.startTimeUnixNano),
|
|
218
248
|
prompt: getStringAttr(attrs, 'user_prompt') ?? '[Claude Code interaction]',
|
|
219
249
|
tokens: { input: inputTokens, output: outputTokens, cached: Number(getAttr(attrs, 'cache_read_tokens') ?? 0), reasoning: 0, written: outputTokens, total: inputTokens + outputTokens },
|
|
220
|
-
aiCredits:
|
|
250
|
+
aiCredits: calcClaudeCredits({ input: inputTokens, output: outputTokens }, model),
|
|
221
251
|
durationMs: Number(getAttr(attrs, 'interaction.duration_ms')
|
|
222
252
|
?? (nanoToMs(span.endTimeUnixNano) - nanoToMs(span.startTimeUnixNano))),
|
|
223
253
|
toolCalls: [],
|
|
@@ -227,6 +257,20 @@ function processSpans(spans, sessionId, projectId, workingDir) {
|
|
|
227
257
|
status: (span.status?.code ?? 0) === 2 ? 'error' : 'done',
|
|
228
258
|
error: span.status?.message,
|
|
229
259
|
};
|
|
260
|
+
// Apply any llm_request delta that arrived before this interaction span.
|
|
261
|
+
const pendingDelta = pendingClaudeLlmDeltas.get(traceId);
|
|
262
|
+
if (pendingDelta) {
|
|
263
|
+
entry.tokens = {
|
|
264
|
+
input: entry.tokens.input + pendingDelta.input,
|
|
265
|
+
output: entry.tokens.output + pendingDelta.output,
|
|
266
|
+
cached: entry.tokens.cached + pendingDelta.cached,
|
|
267
|
+
reasoning: 0,
|
|
268
|
+
written: entry.tokens.written + pendingDelta.output,
|
|
269
|
+
total: entry.tokens.total + pendingDelta.input + pendingDelta.output,
|
|
270
|
+
};
|
|
271
|
+
entry.aiCredits += pendingDelta.credits;
|
|
272
|
+
pendingClaudeLlmDeltas.delete(traceId);
|
|
273
|
+
}
|
|
230
274
|
ensureSession(entry.sessionId, resolveProjectId(getAttr(attrs, 'vcs.repository.url'), detectWorkingDir(attrs) ?? workingDir, projectId));
|
|
231
275
|
upsertTrace(entry);
|
|
232
276
|
rememberClaudeEntry(claudeInteractionEntries, traceId, entry);
|
|
@@ -238,6 +282,7 @@ function processSpans(spans, sessionId, projectId, workingDir) {
|
|
|
238
282
|
const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
|
|
239
283
|
const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
|
|
240
284
|
const cachedTokens = Number(getAttr(attrs, 'cache_read_tokens') ?? 0);
|
|
285
|
+
const model = getStringAttr(attrs, 'model', 'gen_ai.request.model');
|
|
241
286
|
const entry = claudeInteractionEntries.get(traceId);
|
|
242
287
|
if (entry) {
|
|
243
288
|
entry.tokens = {
|
|
@@ -248,10 +293,19 @@ function processSpans(spans, sessionId, projectId, workingDir) {
|
|
|
248
293
|
written: entry.tokens.written + outputTokens,
|
|
249
294
|
total: entry.tokens.total + inputTokens + outputTokens,
|
|
250
295
|
};
|
|
251
|
-
entry.aiCredits +=
|
|
296
|
+
entry.aiCredits += calcClaudeCredits({ input: inputTokens, output: outputTokens }, model);
|
|
252
297
|
upsertTrace(entry);
|
|
253
298
|
traceEvents.emit('trace:update', entry);
|
|
254
299
|
}
|
|
300
|
+
else {
|
|
301
|
+
// Parent claude_code.interaction span hasn't arrived yet — buffer the delta.
|
|
302
|
+
const pending = pendingClaudeLlmDeltas.get(traceId) ?? { input: 0, output: 0, cached: 0, credits: 0 };
|
|
303
|
+
pending.input += inputTokens;
|
|
304
|
+
pending.output += outputTokens;
|
|
305
|
+
pending.cached += cachedTokens;
|
|
306
|
+
pending.credits += calcClaudeCredits({ input: inputTokens, output: outputTokens }, model);
|
|
307
|
+
rememberClaudeEntry(pendingClaudeLlmDeltas, traceId, pending);
|
|
308
|
+
}
|
|
255
309
|
continue;
|
|
256
310
|
}
|
|
257
311
|
// ── invoke_agent span = top-level agent turn ──────────────────────────
|
package/dist/setup.js
CHANGED
|
@@ -147,11 +147,13 @@ function patchShellProfile(profilePath, port) {
|
|
|
147
147
|
const block = otelEnvBlock(port);
|
|
148
148
|
// Already has our block?
|
|
149
149
|
if (content.includes('copilot-tracer OTLP config')) {
|
|
150
|
-
//
|
|
151
|
-
|
|
150
|
+
// Only skip if the embedded block is byte-identical to what we'd generate now —
|
|
151
|
+
// a marker + matching port isn't enough, since the block's env vars can gain new
|
|
152
|
+
// keys (e.g. Claude Code support) between tracer versions without the port changing.
|
|
153
|
+
if (content.includes(block)) {
|
|
152
154
|
return { action: 'already_set' };
|
|
153
155
|
}
|
|
154
|
-
//
|
|
156
|
+
// Block is stale (port changed, or vars were added/changed) — replace it in place
|
|
155
157
|
const updated = content.replace(/# >>> copilot-tracer OTLP config[\s\S]*?# <<< copilot-tracer <<</, block);
|
|
156
158
|
fs.writeFileSync(profilePath, updated, 'utf8');
|
|
157
159
|
return { action: 'updated' };
|
|
@@ -175,10 +177,9 @@ function patchVSCodeSettings(settingsPath, port) {
|
|
|
175
177
|
const envKey = 'terminal.integrated.env.osx';
|
|
176
178
|
const existing = (settings[envKey] ?? {});
|
|
177
179
|
const newEnv = vscodeEnvBlock(port);
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
existing[OTEL_ENABLED_KEY] === 'true') {
|
|
180
|
+
// Already set correctly only if every current key/value is present — checking a
|
|
181
|
+
// hardcoded subset let newer keys (e.g. Claude Code support) silently go unset.
|
|
182
|
+
if (Object.entries(newEnv).every(([k, v]) => existing[k] === v)) {
|
|
182
183
|
return { action: 'already_set' };
|
|
183
184
|
}
|
|
184
185
|
const wasSet = !!existing[OTEL_ENDPOINT_KEY];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-tracer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Real-time tracing and prompt-refinement companion for GitHub Copilot CLI and VS Code Copilot — tracks tokens, AI credits, tool calls, and refined prompts with console and web UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"copilot",
|
package/web/index.html
CHANGED
|
@@ -321,7 +321,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
|
|
|
321
321
|
|
|
322
322
|
<div class="stats-bar">
|
|
323
323
|
<div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
|
|
324
|
-
<div class="stat"><span class="stat-label">
|
|
324
|
+
<div class="stat"><span class="stat-label">Est Cost</span><span class="stat-value credits" id="s-credits">$0.0000</span></div>
|
|
325
325
|
<div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
|
|
326
326
|
<div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
|
|
327
327
|
<div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
|
|
@@ -338,7 +338,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
|
|
|
338
338
|
<tr>
|
|
339
339
|
<th>Date / Time</th>
|
|
340
340
|
<th>Prompt</th>
|
|
341
|
-
<th>
|
|
341
|
+
<th>Est Cost</th>
|
|
342
342
|
<th>Duration</th>
|
|
343
343
|
<th>Cached Tokens</th>
|
|
344
344
|
<th>Written Tokens</th>
|
|
@@ -488,7 +488,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
|
|
|
488
488
|
<div class="dash-total-value tokens">${data.totals.tokens.toLocaleString()}</div>
|
|
489
489
|
</div>
|
|
490
490
|
<div class="dash-total-card">
|
|
491
|
-
<div class="dash-total-label">Total
|
|
491
|
+
<div class="dash-total-label">Total Est Cost</div>
|
|
492
492
|
<div class="dash-total-value credits">${fmtCredits(data.totals.credits)}</div>
|
|
493
493
|
</div>
|
|
494
494
|
`;
|
|
@@ -525,7 +525,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
|
|
|
525
525
|
<span class="dash-project-stat-value tokens">${p.totalTokens.toLocaleString()}</span>
|
|
526
526
|
</div>
|
|
527
527
|
<div class="dash-project-stat">
|
|
528
|
-
<span class="dash-project-stat-label">
|
|
528
|
+
<span class="dash-project-stat-label">Est Cost</span>
|
|
529
529
|
<span class="dash-project-stat-value credits">${fmtCredits(p.totalCredits)}</span>
|
|
530
530
|
</div>
|
|
531
531
|
</div>
|
|
@@ -559,7 +559,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
|
|
|
559
559
|
return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's';
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
function fmtCredits(c) { return
|
|
562
|
+
function fmtCredits(c) { return '$' + ((c||0)*0.01).toFixed(4); }
|
|
563
563
|
|
|
564
564
|
function fmtDateTime(dt) {
|
|
565
565
|
if (!dt) return '—';
|
|
@@ -650,7 +650,7 @@ Written: ${(t.tokens?.written||0).toLocaleString()}
|
|
|
650
650
|
Reasoning: ${(t.tokens?.reasoning||0).toLocaleString()}
|
|
651
651
|
Output: ${(t.tokens?.output||0).toLocaleString()}
|
|
652
652
|
Total: ${(t.tokens?.total||0).toLocaleString()}
|
|
653
|
-
|
|
653
|
+
Est Cost: ${fmtCredits(t.aiCredits)}
|
|
654
654
|
</div>
|
|
655
655
|
</div>
|
|
656
656
|
${t.toolCalls?.length ? `
|
|
@@ -725,14 +725,14 @@ AI Credits: ${fmtCredits(t.aiCredits)}
|
|
|
725
725
|
function showCreditDetail(id) {
|
|
726
726
|
const t = traces[id];
|
|
727
727
|
if (!t) return;
|
|
728
|
-
document.getElementById('modal-title').textContent = `
|
|
728
|
+
document.getElementById('modal-title').textContent = `Est Cost: ${fmtCredits(t.aiCredits)}`;
|
|
729
729
|
document.getElementById('modal-body').innerHTML = `
|
|
730
730
|
<div class="detail-box" style="line-height:2">
|
|
731
731
|
Input tokens: ${(t.tokens?.input||0).toLocaleString()} × $0.003/1k = $${((t.tokens?.input||0)/1000*0.003).toFixed(5)}
|
|
732
732
|
Written tokens: ${(t.tokens?.written||0).toLocaleString()} × $0.006/1k = $${((t.tokens?.written||0)/1000*0.006).toFixed(5)}
|
|
733
733
|
Reasoning tokens: ${(t.tokens?.reasoning||0).toLocaleString()} × $0.009/1k = $${((t.tokens?.reasoning||0)/1000*0.009).toFixed(5)}
|
|
734
734
|
─────────────────────────────────────────
|
|
735
|
-
Total
|
|
735
|
+
Total Est Cost: ${fmtCredits(t.aiCredits)}
|
|
736
736
|
Duration: ${fmtDuration(t.durationMs)}
|
|
737
737
|
</div>
|
|
738
738
|
`;
|