copilot-cli-trace-deck 0.2.0__tar.gz → 0.3.0__tar.gz

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 (20) hide show
  1. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/PKG-INFO +4 -4
  2. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/README.md +3 -3
  3. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/pyproject.toml +1 -1
  4. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/data/sessions.py +70 -6
  5. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/models.py +1 -0
  6. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/pages.py +5 -14
  7. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/PKG-INFO +4 -4
  8. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/LICENSE +0 -0
  9. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/setup.cfg +0 -0
  10. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__init__.py +0 -0
  11. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__main__.py +0 -0
  12. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/data/__init__.py +0 -0
  13. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/server.py +0 -0
  14. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/__init__.py +0 -0
  15. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/server.py +0 -0
  16. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/SOURCES.txt +0 -0
  17. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/dependency_links.txt +0 -0
  18. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/entry_points.txt +0 -0
  19. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/requires.txt +0 -0
  20. {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copilot-cli-trace-deck
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -34,7 +34,7 @@ Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
34
34
  Run the app directly from the workspace with `uv`:
35
35
 
36
36
  ```bash
37
- uv run copilot-cli-trace-deck
37
+ uvx copilot-cli-trace-deck@latest
38
38
  ```
39
39
 
40
40
  By default the server listens on `http://127.0.0.1:9887` and opens that URL in your browser.
@@ -46,13 +46,13 @@ The summary view also estimates per-session GitHub AI Credits / USD cost from sh
46
46
  You can also pass the session-state source and server options:
47
47
 
48
48
  ```bash
49
- uv run copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
49
+ uvx copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
50
50
  ```
51
51
 
52
52
  To skip opening a browser while still printing the local URL:
53
53
 
54
54
  ```bash
55
- uv run copilot-cli-trace-deck --quiet
55
+ uvx copilot-cli-trace-deck --quiet
56
56
  ```
57
57
 
58
58
  ## Install As A Command
@@ -24,7 +24,7 @@ Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
24
24
  Run the app directly from the workspace with `uv`:
25
25
 
26
26
  ```bash
27
- uv run copilot-cli-trace-deck
27
+ uvx copilot-cli-trace-deck@latest
28
28
  ```
29
29
 
30
30
  By default the server listens on `http://127.0.0.1:9887` and opens that URL in your browser.
@@ -36,13 +36,13 @@ The summary view also estimates per-session GitHub AI Credits / USD cost from sh
36
36
  You can also pass the session-state source and server options:
37
37
 
38
38
  ```bash
39
- uv run copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
39
+ uvx copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
40
40
  ```
41
41
 
42
42
  To skip opening a browser while still printing the local URL:
43
43
 
44
44
  ```bash
45
- uv run copilot-cli-trace-deck --quiet
45
+ uvx copilot-cli-trace-deck --quiet
46
46
  ```
47
47
 
48
48
  ## Install As A Command
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "copilot-cli-trace-deck"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -18,6 +18,7 @@ class ModelPricing:
18
18
 
19
19
 
20
20
  AI_CREDIT_USD = Decimal("0.01")
21
+ NANO_AIU_PER_AI_CREDIT = Decimal("1000000000")
21
22
  TOKENS_PER_MILLION = Decimal("1000000")
22
23
  MODEL_PRICING: dict[str, ModelPricing] = {
23
24
  "gpt-4.1": ModelPricing(Decimal("2.00"), Decimal("0.50"), Decimal("8.00")),
@@ -98,7 +99,12 @@ def load_session_summary(session_root: Path, session_id: str) -> SessionSummary
98
99
  model_name = find_current_model(events, shutdown_event)
99
100
  model_usages, billing_stage = extract_model_usages(events, shutdown_event)
100
101
  usage = aggregate_usage(model_usages)
101
- estimated_cost_usd, estimated_ai_credits, billing_note = build_billing_estimate(model_usages, billing_stage)
102
+ shutdown_total_nano_aiu = extract_shutdown_total_nano_aiu(events)
103
+ estimated_cost_usd, estimated_ai_credits, billing_note = build_billing_estimate(
104
+ model_usages,
105
+ billing_stage,
106
+ shutdown_total_nano_aiu=shutdown_total_nano_aiu,
107
+ )
102
108
  created_value = metadata.get("created_at") or first_event_timestamp(events)
103
109
  updated_value = last_event_timestamp(events) or metadata.get("updated_at") or created_value
104
110
 
@@ -239,6 +245,25 @@ def extract_all_shutdown_model_usages(events: list[dict]) -> list[SessionModelUs
239
245
  return merged_model_usages
240
246
 
241
247
 
248
+ def extract_shutdown_total_nano_aiu(events: list[dict]) -> int | None:
249
+ total_nano_aiu = 0
250
+ found_billing = False
251
+ for event in events:
252
+ if event.get("type") != "session.shutdown":
253
+ continue
254
+
255
+ raw_total_nano_aiu = event.get("data", {}).get("totalNanoAiu")
256
+ try:
257
+ total_nano_aiu += int(raw_total_nano_aiu)
258
+ except (TypeError, ValueError):
259
+ continue
260
+ found_billing = True
261
+
262
+ if not found_billing:
263
+ return None
264
+ return total_nano_aiu
265
+
266
+
242
267
  def extract_shutdown_model_usages(shutdown_event: dict | None) -> list[SessionModelUsage]:
243
268
  if not shutdown_event:
244
269
  return []
@@ -255,7 +280,9 @@ def extract_shutdown_model_usages(shutdown_event: dict | None) -> list[SessionMo
255
280
  if not isinstance(usage, dict):
256
281
  continue
257
282
 
258
- model_usage = build_model_usage(model_name, usage)
283
+ estimated_ai_credits = parse_nano_aiu_as_ai_credits(metrics.get("totalNanoAiu"))
284
+
285
+ model_usage = build_model_usage(model_name, usage, estimated_ai_credits=estimated_ai_credits)
259
286
  if model_usage is None:
260
287
  continue
261
288
  model_usages.append(model_usage)
@@ -316,7 +343,12 @@ def extract_live_model_usages_since_last_shutdown(events: list[dict]) -> list[Se
316
343
  return extract_live_model_usages(events[last_shutdown_index + 1 :])
317
344
 
318
345
 
319
- def build_model_usage(model_name: str, usage: dict[str, object]) -> SessionModelUsage | None:
346
+ def build_model_usage(
347
+ model_name: str,
348
+ usage: dict[str, object],
349
+ *,
350
+ estimated_ai_credits: Decimal | None = None,
351
+ ) -> SessionModelUsage | None:
320
352
  if not model_name:
321
353
  return None
322
354
 
@@ -332,6 +364,8 @@ def build_model_usage(model_name: str, usage: dict[str, object]) -> SessionModel
332
364
  cache_write_tokens=cache_write_tokens,
333
365
  output_tokens=output_tokens,
334
366
  )
367
+ if estimated_ai_credits is None and estimated_cost_usd is not None:
368
+ estimated_ai_credits = estimated_cost_usd / AI_CREDIT_USD
335
369
 
336
370
  return SessionModelUsage(
337
371
  model_name=model_name,
@@ -341,11 +375,12 @@ def build_model_usage(model_name: str, usage: dict[str, object]) -> SessionModel
341
375
  output_tokens=output_tokens,
342
376
  total_tokens=total_tokens,
343
377
  estimated_cost_usd=estimated_cost_usd,
378
+ estimated_ai_credits=estimated_ai_credits,
344
379
  )
345
380
 
346
381
 
347
382
  def merge_model_usages(*usage_lists: list[SessionModelUsage]) -> list[SessionModelUsage]:
348
- merged_usage: dict[str, dict[str, int]] = {}
383
+ merged_usage: dict[str, dict[str, object]] = {}
349
384
  for usage_list in usage_lists:
350
385
  for item in usage_list:
351
386
  usage = merged_usage.setdefault(
@@ -355,17 +390,29 @@ def merge_model_usages(*usage_lists: list[SessionModelUsage]) -> list[SessionMod
355
390
  "outputTokens": 0,
356
391
  "cacheReadTokens": 0,
357
392
  "cacheWriteTokens": 0,
393
+ "estimatedAiCredits": None,
358
394
  },
359
395
  )
360
396
  usage["inputTokens"] += item.input_tokens
361
397
  usage["outputTokens"] += item.output_tokens
362
398
  usage["cacheReadTokens"] += item.cached_input_tokens
363
399
  usage["cacheWriteTokens"] += item.cache_write_tokens
400
+ if item.estimated_ai_credits is not None:
401
+ current_estimated_ai_credits = usage.get("estimatedAiCredits")
402
+ if not isinstance(current_estimated_ai_credits, Decimal):
403
+ current_estimated_ai_credits = Decimal("0")
404
+ usage["estimatedAiCredits"] = current_estimated_ai_credits + item.estimated_ai_credits
364
405
 
365
406
  model_usages = [
366
407
  model_usage
367
408
  for model_name, usage in merged_usage.items()
368
- if (model_usage := build_model_usage(model_name, usage)) is not None
409
+ if (
410
+ model_usage := build_model_usage(
411
+ model_name,
412
+ usage,
413
+ estimated_ai_credits=usage.get("estimatedAiCredits") if isinstance(usage.get("estimatedAiCredits"), Decimal) else None,
414
+ )
415
+ ) is not None
369
416
  ]
370
417
  model_usages.sort(key=model_usage_sort_key, reverse=True)
371
418
  return model_usages
@@ -380,6 +427,13 @@ def aggregate_usage(model_usages: list[SessionModelUsage]) -> dict[str, int]:
380
427
  }
381
428
 
382
429
 
430
+ def parse_nano_aiu_as_ai_credits(raw_value: object) -> Decimal | None:
431
+ try:
432
+ return Decimal(int(raw_value)) / NANO_AIU_PER_AI_CREDIT
433
+ except (TypeError, ValueError):
434
+ return None
435
+
436
+
383
437
  def parse_token_count(raw_value: object) -> int:
384
438
  try:
385
439
  return int(raw_value or 0)
@@ -407,12 +461,22 @@ def estimate_model_cost(
407
461
  ) / TOKENS_PER_MILLION
408
462
 
409
463
 
410
- def build_billing_estimate(model_usages: list[SessionModelUsage], billing_stage: str) -> tuple[Decimal | None, Decimal | None, str]:
464
+ def build_billing_estimate(
465
+ model_usages: list[SessionModelUsage],
466
+ billing_stage: str,
467
+ *,
468
+ shutdown_total_nano_aiu: int | None = None,
469
+ ) -> tuple[Decimal | None, Decimal | None, str]:
411
470
  if not model_usages:
412
471
  if billing_stage == "live":
413
472
  return None, None, "Live billing estimate will appear after the first assistant response. Copilot only publishes live output tokens; input and cache token counters refresh on session shutdown."
414
473
  return None, None, "Billing estimate appears after session shutdown publishes model metrics."
415
474
 
475
+ if billing_stage == "shutdown" and shutdown_total_nano_aiu is not None:
476
+ estimated_ai_credits = Decimal(shutdown_total_nano_aiu) / NANO_AIU_PER_AI_CREDIT
477
+ estimated_cost_usd = estimated_ai_credits * AI_CREDIT_USD
478
+ return estimated_cost_usd, estimated_ai_credits, "Estimate uses Copilot session shutdown billing fields."
479
+
416
480
  missing_pricing = [item.model_name for item in model_usages if item.total_tokens and item.estimated_cost_usd is None]
417
481
  known_costs = [item.estimated_cost_usd for item in model_usages if item.estimated_cost_usd is not None]
418
482
  if not known_costs and missing_pricing:
@@ -52,6 +52,7 @@ class SessionModelUsage:
52
52
  output_tokens: int
53
53
  total_tokens: int
54
54
  estimated_cost_usd: Decimal | None
55
+ estimated_ai_credits: Decimal | None
55
56
 
56
57
 
57
58
  @dataclass(frozen=True)
@@ -771,7 +771,6 @@ def build_session_page(summary: SessionSummary) -> str:
771
771
  {render_stat_card(stat_labels['totalCacheWriteLabel'], summary.total_cache_write_tokens, value_id='session-total-cache-write-value', label_id='session-total-cache-write-label')}
772
772
  {render_stat_card('Total Tokens', summary.total_tokens, value_id='session-total-tokens-value')}
773
773
  {render_stat_card('Estimated AI Credits', format_ai_credits(summary.estimated_ai_credits), value_id='session-estimated-ai-credits-value')}
774
- {render_stat_card('Estimated Cost (USD)', format_currency(summary.estimated_cost_usd), value_id='session-estimated-cost-usd-value')}
775
774
  {render_stat_card('Errors', summary.error_count, value_id='session-error-count-value')}
776
775
  </div>
777
776
  <p class="billing-note" id="session-billing-note">{html.escape(summary.billing_note)}</p>
@@ -843,7 +842,6 @@ def build_session_page(summary: SessionSummary) -> str:
843
842
  setText('session-total-cache-write-value', summary.totalCacheWriteTokens || '0');
844
843
  setText('session-total-tokens-value', summary.totalTokens || '0');
845
844
  setText('session-estimated-ai-credits-value', summary.estimatedAiCredits || '-');
846
- setText('session-estimated-cost-usd-value', summary.estimatedCostUsd || '-');
847
845
  setText('session-error-count-value', summary.errorCount || '0');
848
846
  setText('session-billing-note', summary.billingNote || '');
849
847
  setHTML('session-model-usage-breakdown', summary.modelUsageBreakdownHtml || '');
@@ -2400,7 +2398,6 @@ def build_session_snapshot_payload(summary: SessionSummary) -> dict[str, str]:
2400
2398
  "totalCacheWriteTokens": format_number(summary.total_cache_write_tokens),
2401
2399
  "totalTokens": format_number(summary.total_tokens),
2402
2400
  "estimatedAiCredits": format_ai_credits(summary.estimated_ai_credits),
2403
- "estimatedCostUsd": format_currency(summary.estimated_cost_usd),
2404
2401
  "billingNote": summary.billing_note,
2405
2402
  "modelUsageBreakdownHtml": render_model_usage_breakdown(summary),
2406
2403
  "errorCount": format_number(summary.error_count),
@@ -2505,16 +2502,10 @@ def format_stat_value(value: int | str) -> str:
2505
2502
  return format_number(value) if isinstance(value, int) else value
2506
2503
 
2507
2504
 
2508
- def format_currency(value: Decimal | None) -> str:
2509
- if value is None:
2510
- return "-"
2511
- return f"${value.quantize(Decimal('0.000001')):,.6f}".rstrip("0").rstrip(".")
2512
-
2513
-
2514
2505
  def format_ai_credits(value: Decimal | None) -> str:
2515
- if value is None:
2516
- return "-"
2517
- return f"{value.quantize(Decimal('0.0001')):,.4f}".rstrip("0").rstrip(".")
2506
+ if value is None:
2507
+ return "-"
2508
+ return f"{value.quantize(Decimal('0.01')):,.2f}".rstrip("0").rstrip(".")
2518
2509
 
2519
2510
 
2520
2511
  def render_model_usage_breakdown(summary: SessionSummary) -> str:
@@ -2530,7 +2521,7 @@ def render_model_usage_breakdown(summary: SessionSummary) -> str:
2530
2521
  <td>{format_number(item.cache_write_tokens)}</td>
2531
2522
  <td>{format_number(item.output_tokens)}</td>
2532
2523
  <td>{format_number(item.total_tokens)}</td>
2533
- <td>{html.escape(format_currency(item.estimated_cost_usd))}</td>
2524
+ <td>{html.escape(format_ai_credits(item.estimated_ai_credits))}</td>
2534
2525
  </tr>"""
2535
2526
  for item in summary.model_usages
2536
2527
  )
@@ -2546,7 +2537,7 @@ def render_model_usage_breakdown(summary: SessionSummary) -> str:
2546
2537
  <th>Cache Write</th>
2547
2538
  <th>Output</th>
2548
2539
  <th>Total</th>
2549
- <th>Est. USD</th>
2540
+ <th>AIC</th>
2550
2541
  </tr>
2551
2542
  </thead>
2552
2543
  <tbody>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copilot-cli-trace-deck
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -34,7 +34,7 @@ Render GitHub Copilot CLI agent debug logs into a clear, inspectable trace view.
34
34
  Run the app directly from the workspace with `uv`:
35
35
 
36
36
  ```bash
37
- uv run copilot-cli-trace-deck
37
+ uvx copilot-cli-trace-deck@latest
38
38
  ```
39
39
 
40
40
  By default the server listens on `http://127.0.0.1:9887` and opens that URL in your browser.
@@ -46,13 +46,13 @@ The summary view also estimates per-session GitHub AI Credits / USD cost from sh
46
46
  You can also pass the session-state source and server options:
47
47
 
48
48
  ```bash
49
- uv run copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
49
+ uvx copilot-cli-trace-deck ~/.copilot/session-state --host 127.0.0.1 --port 9887
50
50
  ```
51
51
 
52
52
  To skip opening a browser while still printing the local URL:
53
53
 
54
54
  ```bash
55
- uv run copilot-cli-trace-deck --quiet
55
+ uvx copilot-cli-trace-deck --quiet
56
56
  ```
57
57
 
58
58
  ## Install As A Command