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 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 `${c.toFixed(2)} cr | $${(c * 0.01).toFixed(4)}`;
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('AI Credits'),
28
+ chalk.cyan('Est Cost'),
29
29
  chalk.cyan('Duration'),
30
30
  chalk.cyan('Tokens\nCached|Written|Reason'),
31
31
  chalk.cyan('Skills'),
@@ -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: Number(getAttr(attrs, 'cost_usd') ?? 0),
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 += Number(getAttr(attrs, 'cost_usd') ?? 0);
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
- // Check if port matches
151
- if (content.includes(`http://localhost:${port}`)) {
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
- // Port changed — update
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
- // Check if already set correctly
179
- if (existing[OTEL_ENDPOINT_KEY] === `http://localhost:${port}` &&
180
- existing[OTEL_CONTENT_KEY] === 'true' &&
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.6",
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">AI Credits</span><span class="stat-value credits" id="s-credits">0.00 cr | $0.0000</span></div>
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>AI Credits</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 AI Credits</div>
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">AI Credits</span>
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 (c||0).toFixed(2) + ' cr | $' + ((c||0)*0.01).toFixed(4); }
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
- AI Credits: ${fmtCredits(t.aiCredits)}
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 = `AI Credits: ${fmtCredits(t.aiCredits)}`;
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 AI Credits: ${fmtCredits(t.aiCredits)}
735
+ Total Est Cost: ${fmtCredits(t.aiCredits)}
736
736
  Duration: ${fmtDuration(t.durationMs)}
737
737
  </div>
738
738
  `;