pennyrouter 0.3.19 → 0.3.21

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.
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """Write and display the final report for a penny-compare run."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from datetime import datetime, timezone
13
+
14
+
15
+ BUCKETS = ("cache_read", "cache_write", "write_5m", "write_1h", "fresh_input",
16
+ "output", "reasoning_output", "total")
17
+
18
+
19
+ def read_json(path: Path) -> dict:
20
+ try:
21
+ return json.loads(path.read_text())
22
+ except (OSError, ValueError):
23
+ return {}
24
+
25
+
26
+ def read_calls(path: Path, since: float) -> list[dict]:
27
+ calls = []
28
+ try:
29
+ lines = path.read_text().splitlines()
30
+ except OSError:
31
+ return calls
32
+ for line in lines:
33
+ try:
34
+ call = json.loads(line)
35
+ except ValueError:
36
+ continue
37
+ # recorded_at is generated locally when the pane accepts this call. The panes follow
38
+ # transcripts continuously, so it also gives reset a single clock across both formats.
39
+ if float(call.get("recorded_at") or 0) >= since:
40
+ calls.append(call)
41
+ return calls
42
+
43
+
44
+ def summarize(meta: dict, calls: list[dict]) -> dict:
45
+ totals = {bucket: sum(int(call.get(bucket) or 0) for call in calls) for bucket in BUCKETS}
46
+ totals["cost"] = sum(float(call.get("cost") or 0) for call in calls)
47
+ return {**meta, "call_count": len(calls), "totals": totals, "calls": calls}
48
+
49
+
50
+ def metric(saved: float, baseline: float, unit: str = "") -> str:
51
+ pct = (saved / baseline * 100) if baseline else 0.0
52
+ if saved >= 0:
53
+ return f"{unit}{saved:,.2f} saved ({pct:,.1f}%)" if unit else f"{saved:,.0f} saved ({pct:,.1f}%)"
54
+ extra = abs(saved)
55
+ return f"{unit}{extra:,.2f} extra ({abs(pct):,.1f}%)" if unit else f"{extra:,.0f} extra ({abs(pct):,.1f}%)"
56
+
57
+
58
+ def centered(lines: list[str]) -> str:
59
+ size = shutil.get_terminal_size((100, 30))
60
+ width = min(max(len(line) for line in lines) + 8, max(40, size.columns - 2))
61
+ available = width - 6
62
+ lines = [line if len(line) <= available else line[:available - 1] + "…" for line in lines]
63
+ top = max(0, (size.lines - len(lines) - 4) // 2)
64
+ left = max(0, (size.columns - width) // 2)
65
+ pad = " " * left
66
+ border = "═" * (width - 2)
67
+ body = [pad + "╔" + border + "╗"]
68
+ for line in lines:
69
+ inner = line.center(width - 2)
70
+ body.append(pad + "║" + inner + "║")
71
+ body.append(pad + "╚" + border + "╝")
72
+ return "\033[2J\033[H" + ("\n" * top) + "\033[1m" + "\n".join(body) + "\033[0m"
73
+
74
+
75
+ def main() -> int:
76
+ state = Path(sys.argv[1])
77
+ reports = Path(sys.argv[2])
78
+ session = sys.argv[3]
79
+ try:
80
+ started = (state / "started").stat().st_mtime
81
+ except OSError:
82
+ started = 0.0
83
+ try:
84
+ reset = (state / "reset").stat().st_mtime
85
+ except OSError:
86
+ reset = 0.0
87
+ measured_from = max(started, reset)
88
+
89
+ left = summarize(read_json(state / "pane-left.json"),
90
+ read_calls(state / "calls-left.jsonl", measured_from))
91
+ right = summarize(read_json(state / "pane-right.json"),
92
+ read_calls(state / "calls-right.jsonl", measured_from))
93
+ native = next((side for side in (left, right) if side.get("role") == "NATIVE"), left)
94
+ penny = next((side for side in (left, right) if side.get("role") == "PENNY"), right)
95
+ native_totals, penny_totals = native["totals"], penny["totals"]
96
+ savings = {
97
+ "cost": native_totals["cost"] - penny_totals["cost"],
98
+ "tokens": native_totals["total"] - penny_totals["total"],
99
+ }
100
+ savings["cost_percent"] = savings["cost"] / native_totals["cost"] * 100 if native_totals["cost"] else 0.0
101
+ savings["token_percent"] = savings["tokens"] / native_totals["total"] * 100 if native_totals["total"] else 0.0
102
+
103
+ finished = datetime.now(timezone.utc)
104
+ report = {
105
+ "created_at": finished.isoformat(),
106
+ "measured_from_epoch": measured_from,
107
+ "pricing_profile": left.get("pricing_profile", "standard"),
108
+ "left": left, "right": right, "savings_native_minus_penny": savings,
109
+ }
110
+ reports.mkdir(parents=True, exist_ok=True)
111
+ destination = reports / f"compare-{finished.strftime('%Y%m%d-%H%M%S-%f')}.json"
112
+ destination.write_text(json.dumps(report, indent=2) + "\n")
113
+
114
+ lines = [
115
+ "PENNY COMPARISON",
116
+ "",
117
+ metric(savings["cost"], native_totals["cost"], "$"),
118
+ metric(savings["tokens"], native_totals["total"]),
119
+ "",
120
+ f"NATIVE {native['call_count']} calls {native_totals['total']:,} tokens ${native_totals['cost']:,.2f}",
121
+ f"PENNY {penny['call_count']} calls {penny_totals['total']:,} tokens ${penny_totals['cost']:,.2f}",
122
+ "",
123
+ f"Report: {destination}",
124
+ "Press Enter to quit",
125
+ ]
126
+ sys.stdout.write(centered(lines))
127
+ sys.stdout.flush()
128
+ try:
129
+ input()
130
+ except (EOFError, KeyboardInterrupt):
131
+ pass
132
+ subprocess.run(["tmux", "kill-session", "-t", session], check=False)
133
+ return 0
134
+
135
+
136
+ if __name__ == "__main__":
137
+ raise SystemExit(main())