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.
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/PKG-INFO +4 -4
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/README.md +3 -3
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/pyproject.toml +1 -1
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/data/sessions.py +70 -6
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/models.py +1 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/pages.py +5 -14
- {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
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/LICENSE +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/setup.cfg +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__init__.py +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__main__.py +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/data/__init__.py +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/server.py +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/__init__.py +0 -0
- {copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/web/server.py +0 -0
- {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
- {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
- {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
- {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
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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 (
|
|
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(
|
|
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:
|
|
@@ -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
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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(
|
|
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>
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
uvx copilot-cli-trace-deck --quiet
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Install As A Command
|
|
File without changes
|
|
File without changes
|
{copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__init__.py
RENAMED
|
File without changes
|
{copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{copilot_cli_trace_deck-0.2.0 → copilot_cli_trace_deck-0.3.0}/src/copilot_cli_trace_deck/server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|