aimpg 0.6.0__tar.gz → 0.6.2__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.
- {aimpg-0.6.0 → aimpg-0.6.2}/PKG-INFO +27 -18
- {aimpg-0.6.0 → aimpg-0.6.2}/README.md +26 -17
- {aimpg-0.6.0 → aimpg-0.6.2}/action.yml +3 -1
- aimpg-0.6.2/aimpg/card.py +151 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cli.py +22 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/coach.py +22 -6
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cost.py +3 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/live.py +14 -4
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/prices.json +17 -2
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/receipt.py +4 -2
- aimpg-0.6.2/aimpg/replay/chain.py +67 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/cli.py +25 -4
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/fake_agent.py +10 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/run.py +44 -13
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/sandbox.py +25 -6
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/select.py +24 -6
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/setups.py +45 -2
- aimpg-0.6.2/aimpg/replay/study.py +215 -0
- aimpg-0.6.2/aimpg/replay/study_cli.py +176 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/verify.py +35 -11
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/workspace.py +71 -10
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/scoreboard.py +6 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/share.py +1 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/team.py +3 -2
- aimpg-0.6.2/docs/FAQ.md +56 -0
- aimpg-0.6.2/docs/designs/launch-brief.md +82 -0
- aimpg-0.6.2/docs/receipt-card.png +0 -0
- aimpg-0.6.2/docs/study/report-1/PREREGISTRATION.md +73 -0
- aimpg-0.6.2/docs/study/report-1/plan.json +28 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/pyproject.toml +2 -2
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/conftest.py +3 -0
- aimpg-0.6.2/tests/test_card.py +31 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_coach.py +2 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_cost_and_equivalence.py +10 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_live.py +1 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_e2e.py +26 -0
- aimpg-0.6.2/tests/test_security.py +96 -0
- aimpg-0.6.2/tests/test_study.py +101 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_verify.py +14 -3
- {aimpg-0.6.0 → aimpg-0.6.2}/uv.lock +1 -1
- {aimpg-0.6.0 → aimpg-0.6.2}/.github/workflows/publish.yml +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/.github/workflows/test.yml +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/.gitignore +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/LICENSE +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/TODOS.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/__init__.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/attribution.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/codex_logs.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cursor_usage.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/defaults.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/durable.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/energy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/equivalence.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/equivalences.json +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/factors.json +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/gitkept.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/ledger.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/logs.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/model.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/__init__.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/hints.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/picker.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/proxy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/stats.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/sci.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/docs/STRATEGY.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/docs/designs/aimpg-design.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/docs/designs/scoreboard-brief.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/evals/attribution_eval.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/evals/label.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/RESULTS.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/allowlist_proxy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/make_profile.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/RESULTS.md +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/allowlist_proxy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/replay.sb +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/gitrepo.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_attribution.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_codex_logs.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_cursor_usage.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_defaults.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_durable.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_energy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_eval_scoring.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_gitkept.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_logs.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_perf.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_cli.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_hints.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_picker.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_proxy.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_sandbox.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_stats.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_sandbox_required.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_sci.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_scoreboard.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_share.py +0 -0
- {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_team.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aimpg
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Real-world energy per solved task for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/kumarganduri/aimpg
|
|
6
6
|
Project-URL: Issues, https://github.com/kumarganduri/aimpg/issues
|
|
@@ -26,25 +26,27 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
uvx aimpg report
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
**New here? [aimpg in 5 minutes](https://kumarganduri.github.io/aimpg-scoreboard/start/)** walks you from install to your first numbers, the live status line, and your team's GitHub Action, with copy buttons for every command.
|
|
30
|
+
|
|
29
31
|
```
|
|
30
32
|
YOUR AI CODING
|
|
31
|
-
Energy
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Output 353 commits made with AI help
|
|
33
|
+
Energy 17.31 kWh – 156.95 kWh
|
|
34
|
+
Money $1,567 at API list prices (Anthropic's and OpenAI's; a subscription costs less)
|
|
35
|
+
Output 454 commits made with AI help
|
|
35
36
|
|
|
36
|
-
A typical kept commit:
|
|
37
|
+
A typical kept commit: 20.0 Wh – 179.3 Wh · $1.63
|
|
37
38
|
|
|
38
39
|
WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)
|
|
39
|
-
1. Start a fresh session after each commit: up to
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
1. Start a fresh session after each commit: up to 41%–46% less energy, up to $457
|
|
41
|
+
57% of the tokens your agents processed were conversation carried over from before your last commit.
|
|
42
|
+
A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller.
|
|
43
|
+
→ After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed.
|
|
44
|
+
2. Use a mid-size model for routine work: up to 17% less energy, $408
|
|
45
|
+
95% of your AI energy ran on the largest models (Opus/Fable class).
|
|
46
|
+
→ Try Sonnet for routine fixes; check it's good enough on your own commits with `aimpg replay` (model picker).
|
|
45
47
|
|
|
46
48
|
WORKING CHANGES (commits that shipped and lasted 30 days)
|
|
47
|
-
Available on 2026-10-
|
|
49
|
+
Available on 2026-10-13: aimpg is keeping your history so it can tell.
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
*(Real output from the author's last 30 days.)*
|
|
@@ -68,7 +70,11 @@ aimpg report --cursor-usage ~/Downloads/usage-events.csv --cursor-repo ~/code/my
|
|
|
68
70
|
Team export? Add `--cursor-user "Your Name"` to count only your rows.
|
|
69
71
|
|
|
70
72
|
### 1. See your AI footprint in terms you can picture
|
|
71
|
-
`aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the
|
|
73
|
+
`aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the cost at Anthropic's and OpenAI's API list prices; on a subscription you pay less, so read it as the value of the work, not your bill.
|
|
74
|
+
|
|
75
|
+
`aimpg report --share` also writes a card to post, with totals only (no repo names, commit messages or code), and prints a one-line post to go with it:
|
|
76
|
+
|
|
77
|
+

|
|
72
78
|
|
|
73
79
|
### 2. Get tips measured on your own habits, not generic advice
|
|
74
80
|
The receipt replays your own logs under "what if" rules and tells you what would have saved the most, in Wh and dollars. For example: how much energy went into re-reading old conversation in long sessions, or into running the biggest model for routine fixes.
|
|
@@ -113,7 +119,7 @@ VERDICT: NOT PROVEN
|
|
|
113
119
|
|
|
114
120
|
Energy, same tasks: +10% (interval -11%…+35%, 17 shared solved commits)
|
|
115
121
|
```
|
|
116
|
-
That's the real result on the author's Legwork repo: no saving shown,
|
|
122
|
+
That's the real result on the author's Legwork repo: no reliable energy saving shown on one repo (RTK advertises token savings; a larger, pre-registered study across several public repos is under way). Your repo may differ, and now you can check.
|
|
117
123
|
|
|
118
124
|
Every run writes a **record** (`~/.aimpg/verify/*.record.json`) holding versions, both setups, each run's tokens, $ and outcome, and the verdict, but never code, diffs or commit messages. Repo and commits are hashed unless you add `--public`. Anyone can recheck the math and the record's consistency for free with `aimpg verify --check record.json` (it can't prove the runs happened), and rerun a public record on their own machine with `aimpg verify --rerun record.json --repo <clone>`, which first shows every command, hook and prompt the record would run and asks before running them. Private records keep only fingerprints (SHA-256) of your prompts, CLAUDE.md, settings and commands, never the text. Records also list every redone attempt and excluded commit.
|
|
119
125
|
|
|
@@ -147,7 +153,7 @@ jobs:
|
|
|
147
153
|
steps:
|
|
148
154
|
- uses: actions/checkout@v4
|
|
149
155
|
with: {fetch-depth: 0}
|
|
150
|
-
- uses: kumarganduri/aimpg@v0.6.
|
|
156
|
+
- uses: kumarganduri/aimpg@v0.6.2 # or pin a commit SHA: it runs with your repo token
|
|
151
157
|
with:
|
|
152
158
|
mode: ${{ github.event_name == 'pull_request' && 'pr' || 'rollup' }}
|
|
153
159
|
comment: "true"
|
|
@@ -177,14 +183,17 @@ For a standard figure, `aimpg sci --json sci.json` reports your carbon intensity
|
|
|
177
183
|
|
|
178
184
|
**Working changes:** aimpg keeps a small history of what each commit cost (never prompts or code), because Claude Code deletes its own logs after 30 days. Once a commit is 30 days old, it is judged: still on the main branch and not reverted or largely rewritten counts as a *working change*. The receipt then shows **cost per working change** and a **waste ratio** (AI spend on work that didn't last).
|
|
179
185
|
|
|
180
|
-
**Energy:** a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
|
|
186
|
+
**Energy:** an estimate with stated assumption ranges, not a measurement: a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
|
|
181
187
|
|
|
182
188
|
**Replays:** each run starts from the code just before your commit, in a fresh, history-free copy, inside a sandbox (Seatbelt on macOS, bubblewrap on Linux). The only network allowed is the model API, through an allowlisting proxy. Your own tests judge the result, and edited tests are always restored before judging. Every token count is cross-checked against Claude Code's own totals. Details are in [docs/designs/aimpg-design.md](docs/designs/aimpg-design.md).
|
|
183
189
|
|
|
190
|
+
Questions people ask (is it sponsored, are the energy numbers guesses, is it safe to run): see the [FAQ](docs/FAQ.md).
|
|
191
|
+
|
|
184
192
|
## Limits (honest list)
|
|
185
193
|
|
|
186
194
|
- Claude Code and Codex CLI logs are read automatically. Cursor keeps token usage on its servers, so it needs its usage export (below), and that export doesn't say which folder the work was in: give `--cursor-repo` and requests are matched to your next own commit there by time alone (shown as "by time only"). Replays and `verify` need macOS or Linux and an API key (about $0.10–0.30 per run). On Linux install bubblewrap (`sudo apt install bubblewrap`); Ubuntu 24.04+ also needs a one-time AppArmor rule for bwrap, which aimpg prints if it's missing.
|
|
187
|
-
-
|
|
195
|
+
- Windows: `report`, `live` and the team commands are untested there; replays and `verify` need macOS or Linux.
|
|
196
|
+
- Energy is an estimate with a wide range (model sizes are guesses within stated bounds); dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
|
|
188
197
|
- Tips are upper bounds and overlap; they can't be added together.
|
|
189
198
|
- Replays from commit messages alone are hard (12% solved on the author's repo); `--task-mode tests` shows the agent the tests, which makes tasks easier than real work but keeps comparisons fair.
|
|
190
199
|
|
|
@@ -6,25 +6,27 @@
|
|
|
6
6
|
uvx aimpg report
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
**New here? [aimpg in 5 minutes](https://kumarganduri.github.io/aimpg-scoreboard/start/)** walks you from install to your first numbers, the live status line, and your team's GitHub Action, with copy buttons for every command.
|
|
10
|
+
|
|
9
11
|
```
|
|
10
12
|
YOUR AI CODING
|
|
11
|
-
Energy
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Output 353 commits made with AI help
|
|
13
|
+
Energy 17.31 kWh – 156.95 kWh
|
|
14
|
+
Money $1,567 at API list prices (Anthropic's and OpenAI's; a subscription costs less)
|
|
15
|
+
Output 454 commits made with AI help
|
|
15
16
|
|
|
16
|
-
A typical kept commit:
|
|
17
|
+
A typical kept commit: 20.0 Wh – 179.3 Wh · $1.63
|
|
17
18
|
|
|
18
19
|
WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)
|
|
19
|
-
1. Start a fresh session after each commit: up to
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
1. Start a fresh session after each commit: up to 41%–46% less energy, up to $457
|
|
21
|
+
57% of the tokens your agents processed were conversation carried over from before your last commit.
|
|
22
|
+
A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller.
|
|
23
|
+
→ After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed.
|
|
24
|
+
2. Use a mid-size model for routine work: up to 17% less energy, $408
|
|
25
|
+
95% of your AI energy ran on the largest models (Opus/Fable class).
|
|
26
|
+
→ Try Sonnet for routine fixes; check it's good enough on your own commits with `aimpg replay` (model picker).
|
|
25
27
|
|
|
26
28
|
WORKING CHANGES (commits that shipped and lasted 30 days)
|
|
27
|
-
Available on 2026-10-
|
|
29
|
+
Available on 2026-10-13: aimpg is keeping your history so it can tell.
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
*(Real output from the author's last 30 days.)*
|
|
@@ -48,7 +50,11 @@ aimpg report --cursor-usage ~/Downloads/usage-events.csv --cursor-repo ~/code/my
|
|
|
48
50
|
Team export? Add `--cursor-user "Your Name"` to count only your rows.
|
|
49
51
|
|
|
50
52
|
### 1. See your AI footprint in terms you can picture
|
|
51
|
-
`aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the
|
|
53
|
+
`aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the cost at Anthropic's and OpenAI's API list prices; on a subscription you pay less, so read it as the value of the work, not your bill.
|
|
54
|
+
|
|
55
|
+
`aimpg report --share` also writes a card to post, with totals only (no repo names, commit messages or code), and prints a one-line post to go with it:
|
|
56
|
+
|
|
57
|
+

|
|
52
58
|
|
|
53
59
|
### 2. Get tips measured on your own habits, not generic advice
|
|
54
60
|
The receipt replays your own logs under "what if" rules and tells you what would have saved the most, in Wh and dollars. For example: how much energy went into re-reading old conversation in long sessions, or into running the biggest model for routine fixes.
|
|
@@ -93,7 +99,7 @@ VERDICT: NOT PROVEN
|
|
|
93
99
|
|
|
94
100
|
Energy, same tasks: +10% (interval -11%…+35%, 17 shared solved commits)
|
|
95
101
|
```
|
|
96
|
-
That's the real result on the author's Legwork repo: no saving shown,
|
|
102
|
+
That's the real result on the author's Legwork repo: no reliable energy saving shown on one repo (RTK advertises token savings; a larger, pre-registered study across several public repos is under way). Your repo may differ, and now you can check.
|
|
97
103
|
|
|
98
104
|
Every run writes a **record** (`~/.aimpg/verify/*.record.json`) holding versions, both setups, each run's tokens, $ and outcome, and the verdict, but never code, diffs or commit messages. Repo and commits are hashed unless you add `--public`. Anyone can recheck the math and the record's consistency for free with `aimpg verify --check record.json` (it can't prove the runs happened), and rerun a public record on their own machine with `aimpg verify --rerun record.json --repo <clone>`, which first shows every command, hook and prompt the record would run and asks before running them. Private records keep only fingerprints (SHA-256) of your prompts, CLAUDE.md, settings and commands, never the text. Records also list every redone attempt and excluded commit.
|
|
99
105
|
|
|
@@ -127,7 +133,7 @@ jobs:
|
|
|
127
133
|
steps:
|
|
128
134
|
- uses: actions/checkout@v4
|
|
129
135
|
with: {fetch-depth: 0}
|
|
130
|
-
- uses: kumarganduri/aimpg@v0.6.
|
|
136
|
+
- uses: kumarganduri/aimpg@v0.6.2 # or pin a commit SHA: it runs with your repo token
|
|
131
137
|
with:
|
|
132
138
|
mode: ${{ github.event_name == 'pull_request' && 'pr' || 'rollup' }}
|
|
133
139
|
comment: "true"
|
|
@@ -157,14 +163,17 @@ For a standard figure, `aimpg sci --json sci.json` reports your carbon intensity
|
|
|
157
163
|
|
|
158
164
|
**Working changes:** aimpg keeps a small history of what each commit cost (never prompts or code), because Claude Code deletes its own logs after 30 days. Once a commit is 30 days old, it is judged: still on the main branch and not reverted or largely rewritten counts as a *working change*. The receipt then shows **cost per working change** and a **waste ratio** (AI spend on work that didn't last).
|
|
159
165
|
|
|
160
|
-
**Energy:** a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
|
|
166
|
+
**Energy:** an estimate with stated assumption ranges, not a measurement: a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
|
|
161
167
|
|
|
162
168
|
**Replays:** each run starts from the code just before your commit, in a fresh, history-free copy, inside a sandbox (Seatbelt on macOS, bubblewrap on Linux). The only network allowed is the model API, through an allowlisting proxy. Your own tests judge the result, and edited tests are always restored before judging. Every token count is cross-checked against Claude Code's own totals. Details are in [docs/designs/aimpg-design.md](docs/designs/aimpg-design.md).
|
|
163
169
|
|
|
170
|
+
Questions people ask (is it sponsored, are the energy numbers guesses, is it safe to run): see the [FAQ](docs/FAQ.md).
|
|
171
|
+
|
|
164
172
|
## Limits (honest list)
|
|
165
173
|
|
|
166
174
|
- Claude Code and Codex CLI logs are read automatically. Cursor keeps token usage on its servers, so it needs its usage export (below), and that export doesn't say which folder the work was in: give `--cursor-repo` and requests are matched to your next own commit there by time alone (shown as "by time only"). Replays and `verify` need macOS or Linux and an API key (about $0.10–0.30 per run). On Linux install bubblewrap (`sudo apt install bubblewrap`); Ubuntu 24.04+ also needs a one-time AppArmor rule for bwrap, which aimpg prints if it's missing.
|
|
167
|
-
-
|
|
175
|
+
- Windows: `report`, `live` and the team commands are untested there; replays and `verify` need macOS or Linux.
|
|
176
|
+
- Energy is an estimate with a wide range (model sizes are guesses within stated bounds); dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
|
|
168
177
|
- Tips are upper bounds and overlap; they can't be added together.
|
|
169
178
|
- Replays from commit messages alone are hard (12% solved on the author's repo); `--task-mode tests` shows the agent the tests, which makes tasks easier than real work but keeps comparisons fair.
|
|
170
179
|
|
|
@@ -27,7 +27,9 @@ outputs:
|
|
|
27
27
|
runs:
|
|
28
28
|
using: composite
|
|
29
29
|
steps:
|
|
30
|
-
- uses: astral-sh/setup-uv@v6
|
|
30
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6, pinned: users run this with their repo's token
|
|
31
|
+
with:
|
|
32
|
+
version: "0.11.29"
|
|
31
33
|
- name: fetch aimpg notes
|
|
32
34
|
shell: bash
|
|
33
35
|
working-directory: ${{ inputs.path }}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""`aimpg report --share`: a receipt card for social posts (1200×630, the size link
|
|
2
|
+
previews use). Totals only: never repo names, commit messages, paths or code.
|
|
3
|
+
|
|
4
|
+
Writes an SVG (always) and a PNG next to it when the machine can render one
|
|
5
|
+
(macOS Quick Look, rsvg-convert, or cairosvg), and returns a one-line post.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import html
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import statistics
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from aimpg.coach import tips as coach_tips
|
|
21
|
+
from aimpg.cost import total_cost
|
|
22
|
+
from aimpg.energy import total_wh
|
|
23
|
+
from aimpg.receipt import KEPT, task_energy
|
|
24
|
+
|
|
25
|
+
W, H = 1200, 630
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CardNumbers:
|
|
30
|
+
days: int
|
|
31
|
+
usd: float
|
|
32
|
+
commits: int
|
|
33
|
+
kwh: tuple[float, float]
|
|
34
|
+
typical_usd: float | None
|
|
35
|
+
typical_label: str
|
|
36
|
+
tip: str | None
|
|
37
|
+
cost_per_working: float | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def numbers(parsed, attribution, since: float, now: float, judged=None) -> CardNumbers:
|
|
41
|
+
in_window = [r for r in parsed.requests if r.ts >= since]
|
|
42
|
+
energies = task_energy(attribution.tasks)
|
|
43
|
+
usd, _ = total_cost(in_window)
|
|
44
|
+
wh = total_wh(in_window)
|
|
45
|
+
with_work = [e for e in energies if e.task.requests]
|
|
46
|
+
kept = [e for e in with_work if e.task.status in KEPT]
|
|
47
|
+
pool = kept if len(kept) >= 5 else with_work
|
|
48
|
+
typical = statistics.median(e.usd for e in pool) if pool else None
|
|
49
|
+
found = coach_tips(in_window, attribution)
|
|
50
|
+
tip = None
|
|
51
|
+
fresh = next((t for t in found if t.title.startswith("Start a fresh session")), None)
|
|
52
|
+
pct = re.match(r"\d+%", fresh.measured) if fresh else None
|
|
53
|
+
if pct:
|
|
54
|
+
tip = f"{pct.group(0)} of my agents' tokens re-read conversation from before my last commit"
|
|
55
|
+
cpw = judged.cost_per_durable() if judged is not None and judged.judged else None
|
|
56
|
+
return CardNumbers(round((now - since) / 86400), usd, len(energies), (wh.low / 1000, wh.high / 1000), typical,
|
|
57
|
+
"per kept commit" if pool is kept else "per commit", tip, cpw)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _money(x: float) -> str:
|
|
61
|
+
return f"${x:,.0f}" if x >= 100 else f"${x:,.2f}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def svg(n: CardNumbers) -> str:
|
|
65
|
+
e = html.escape
|
|
66
|
+
big = _money(n.cost_per_working) if n.cost_per_working is not None else (_money(n.typical_usd) if n.typical_usd else "—")
|
|
67
|
+
big_label = "per working change (still standing after 30 days)" if n.cost_per_working is not None else n.typical_label
|
|
68
|
+
kwh = f"{n.kwh[0]:,.1f}–{n.kwh[1]:,.0f} kWh" if n.kwh[1] >= 10 else f"{n.kwh[0]:,.2f}–{n.kwh[1]:,.1f} kWh"
|
|
69
|
+
rows = [("AI spend at API list prices", _money(n.usd)), ("Commits made with AI help", f"{n.commits:,}"),
|
|
70
|
+
("Energy (estimate, range)", kwh)]
|
|
71
|
+
row_svg = "".join(
|
|
72
|
+
f'<text x="72" y="{392 + i * 52}" class="k">{e(k)}</text><text x="1128" y="{392 + i * 52}" class="v" text-anchor="end">{e(v)}</text>'
|
|
73
|
+
f'<line x1="72" y1="{408 + i * 52}" x2="1128" y2="{408 + i * 52}" class="rule"/>'
|
|
74
|
+
for i, (k, v) in enumerate(rows))
|
|
75
|
+
tip = f'<text x="72" y="566" class="tip">↺ {e(n.tip)}</text>' if n.tip else ""
|
|
76
|
+
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">
|
|
77
|
+
<style>
|
|
78
|
+
.bg{{fill:#10292a}} .k{{font:500 26px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#9fb9b2}}
|
|
79
|
+
.v{{font:600 28px "IBM Plex Mono",Menlo,monospace;fill:#e2ece8}} .rule{{stroke:#24433f;stroke-width:1.5}}
|
|
80
|
+
.eyebrow{{font:600 22px "IBM Plex Mono",Menlo,monospace;fill:#f0ad3a;letter-spacing:3px}}
|
|
81
|
+
.big{{font:800 132px "Archivo","Helvetica Neue",Arial,sans-serif;fill:#ffffff}}
|
|
82
|
+
.biglabel{{font:500 30px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#e2ece8}}
|
|
83
|
+
.tip{{font:500 24px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#f0ad3a}}
|
|
84
|
+
.foot{{font:500 22px "IBM Plex Mono",Menlo,monospace;fill:#9fb9b2}}
|
|
85
|
+
</style>
|
|
86
|
+
<rect class="bg" width="{W}" height="{H}"/>
|
|
87
|
+
<rect x="0" y="0" width="{W}" height="8" fill="#f0ad3a"/>
|
|
88
|
+
<text x="72" y="92" class="eyebrow">MY AI CODING · LAST {n.days} DAYS</text>
|
|
89
|
+
<text x="66" y="236" class="big">{e(big)}</text>
|
|
90
|
+
<text x="72" y="290" class="biglabel">{e(big_label)}</text>
|
|
91
|
+
{row_svg}
|
|
92
|
+
{tip}
|
|
93
|
+
<text x="1128" y="606" class="foot" text-anchor="end">see yours: uvx aimpg report</text>
|
|
94
|
+
</svg>
|
|
95
|
+
'''
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def post_text(n: CardNumbers) -> str:
|
|
99
|
+
head = (f"My AI coding costs {_money(n.cost_per_working)} per working change"
|
|
100
|
+
if n.cost_per_working is not None else
|
|
101
|
+
f"My AI coding: {_money(n.typical_usd)} {n.typical_label}" if n.typical_usd else "My AI coding")
|
|
102
|
+
return (f"{head} ({_money(n.usd)} over {n.commits} commits in {n.days} days, at API list prices). "
|
|
103
|
+
"Measured from my own Claude Code/Codex logs with aimpg. See yours: uvx aimpg report")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
CHROMES = ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
107
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
108
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
109
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
110
|
+
"google-chrome", "chromium", "chromium-browser", "microsoft-edge")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def to_png(svg_path: Path) -> Path | None:
|
|
114
|
+
"""Best effort, no new dependencies: rsvg-convert, a headless Chrome-family browser, or cairosvg."""
|
|
115
|
+
png = svg_path.with_suffix(".png")
|
|
116
|
+
if shutil.which("rsvg-convert"):
|
|
117
|
+
if subprocess.run(["rsvg-convert", "-w", str(W), "-o", str(png), str(svg_path)], capture_output=True).returncode == 0:
|
|
118
|
+
return png
|
|
119
|
+
for browser in CHROMES:
|
|
120
|
+
exe = browser if Path(browser).exists() else shutil.which(browser)
|
|
121
|
+
if not exe:
|
|
122
|
+
continue
|
|
123
|
+
png.unlink(missing_ok=True)
|
|
124
|
+
with tempfile.TemporaryDirectory() as profile:
|
|
125
|
+
proc = subprocess.Popen([exe, "--headless", "--disable-gpu", "--hide-scrollbars", "--no-first-run",
|
|
126
|
+
"--no-default-browser-check", "--disable-extensions", f"--user-data-dir={profile}",
|
|
127
|
+
f"--window-size={W},{H}", f"--screenshot={png}", svg_path.resolve().as_uri()],
|
|
128
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
|
129
|
+
# Chrome sometimes writes the screenshot and then doesn't exit: wait for the file, then stop it
|
|
130
|
+
deadline = time.time() + 60
|
|
131
|
+
while time.time() < deadline and proc.poll() is None and not (png.exists() and png.stat().st_size > 0):
|
|
132
|
+
time.sleep(0.25)
|
|
133
|
+
time.sleep(0.5)
|
|
134
|
+
if proc.poll() is None:
|
|
135
|
+
proc.kill()
|
|
136
|
+
proc.wait(timeout=10)
|
|
137
|
+
if png.exists() and png.stat().st_size > 0:
|
|
138
|
+
return png
|
|
139
|
+
try:
|
|
140
|
+
import cairosvg # type: ignore
|
|
141
|
+
|
|
142
|
+
cairosvg.svg2png(url=str(svg_path), write_to=str(png), output_width=W)
|
|
143
|
+
return png
|
|
144
|
+
except Exception:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def write(n: CardNumbers, path: Path) -> tuple[Path, Path | None]:
|
|
149
|
+
path = path.with_suffix(".svg")
|
|
150
|
+
path.write_text(svg(n))
|
|
151
|
+
return path, to_png(path)
|
|
@@ -53,7 +53,10 @@ def _analyze(args):
|
|
|
53
53
|
def main(argv: list[str] | None = None) -> int:
|
|
54
54
|
parser = argparse.ArgumentParser(prog="aimpg", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
55
55
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
56
|
-
|
|
56
|
+
report_p = sub.add_parser("report", help="print the energy receipt")
|
|
57
|
+
_common(report_p, 30)
|
|
58
|
+
report_p.add_argument("--share", type=Path, nargs="?", const=Path("aimpg-receipt.svg"), default=None, metavar="FILE",
|
|
59
|
+
help="also write a share card (totals only; SVG, plus PNG where possible)")
|
|
57
60
|
|
|
58
61
|
pr = sub.add_parser("pr", help="AI energy and cost of this branch's commits, as a PR comment")
|
|
59
62
|
_common(pr, 90)
|
|
@@ -98,6 +101,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
98
101
|
verify_mod.add_parser(sub)
|
|
99
102
|
scoreboard_mod.add_parsers(sub)
|
|
100
103
|
defaults_mod.add_parser(sub)
|
|
104
|
+
from aimpg.replay import study_cli
|
|
105
|
+
|
|
106
|
+
study_cli.add_parser(sub)
|
|
101
107
|
args = parser.parse_args(argv)
|
|
102
108
|
if args.command == "replay":
|
|
103
109
|
return replay_cli.main(args)
|
|
@@ -107,6 +113,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
107
113
|
return scoreboard_mod.main(args)
|
|
108
114
|
if args.command == "defaults":
|
|
109
115
|
return defaults_mod.main(args)
|
|
116
|
+
if args.command == "study":
|
|
117
|
+
from aimpg.replay import study_cli
|
|
118
|
+
|
|
119
|
+
return study_cli.main(args)
|
|
110
120
|
if args.command in ("statusline", "hook", "live"):
|
|
111
121
|
from aimpg import live as live_mod
|
|
112
122
|
|
|
@@ -219,6 +229,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
219
229
|
ledger.record(task_energy(attribution.tasks))
|
|
220
230
|
judged = durable.judge(ledger.load(), now)
|
|
221
231
|
sys.stdout.write(render(parsed, attribution, since, now, judged))
|
|
232
|
+
if args.share:
|
|
233
|
+
from aimpg import card
|
|
234
|
+
|
|
235
|
+
n = card.numbers(parsed, attribution, since, now, judged)
|
|
236
|
+
try:
|
|
237
|
+
svg_path, png_path = card.write(n, args.share)
|
|
238
|
+
except Exception as exc: # the receipt above is already printed; a renderer problem must not hide it
|
|
239
|
+
svg_path, png_path = args.share.with_suffix(".svg"), None
|
|
240
|
+
print(f"\n(Couldn't render the PNG: {exc}; the SVG is still written.)")
|
|
241
|
+
print(f"\nShare card: {png_path or svg_path} (totals only: no repo names, commit messages or code)")
|
|
242
|
+
print(f"Post text: {card.post_text(n)}")
|
|
222
243
|
return 0
|
|
223
244
|
|
|
224
245
|
|
|
@@ -12,9 +12,12 @@ the receipt, so it's never generic advice:
|
|
|
12
12
|
request after c2 re-reads: base + (everything up to c2) + new work
|
|
13
13
|
fresh session after c2: base + new work
|
|
14
14
|
|
|
15
|
-
The carried-over part is removed from that request's cache reads
|
|
16
|
-
the
|
|
17
|
-
|
|
15
|
+
The carried-over part is removed from that request's cache reads. What's
|
|
16
|
+
measured is the share of tokens carried over; the saving is an upper bound,
|
|
17
|
+
because a fresh session has to rebuild some context (re-reading files,
|
|
18
|
+
CLAUDE.md, a summary), which this doesn't charge. In energy it depends on
|
|
19
|
+
what re-reading cached context costs, so it's given as a range across the
|
|
20
|
+
energy assumptions, from nearly nothing (cache still in fast memory) to a lot.
|
|
18
21
|
|
|
19
22
|
2. Model size. Requests on large-class models (Opus, Fable) re-priced and
|
|
20
23
|
re-estimated as if they had run on a mid-class model (Sonnet). Also an upper
|
|
@@ -42,7 +45,9 @@ class Tip:
|
|
|
42
45
|
action: str # what to do
|
|
43
46
|
saving_wh: WhRange
|
|
44
47
|
saving_usd: float
|
|
45
|
-
share: float # of the window's energy (mid estimate)
|
|
48
|
+
share: float # of the window's energy (mid estimate), for ordering
|
|
49
|
+
saving_text: str = "" # how the saving is stated, when a single % would overclaim
|
|
50
|
+
caveat: str = ""
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
def _sum(requests: list[Request]) -> tuple[WhRange, float]:
|
|
@@ -64,6 +69,7 @@ def fresh_session_tip(requests: list[Request], attribution: Attribution) -> Tip
|
|
|
64
69
|
by_session[r.session_id].append(r)
|
|
65
70
|
|
|
66
71
|
actual, simulated = [], []
|
|
72
|
+
carried_tokens = 0
|
|
67
73
|
for session, reqs in by_session.items():
|
|
68
74
|
reqs.sort(key=lambda r: r.ts)
|
|
69
75
|
main = [r for r in reqs if not r.is_sidechain]
|
|
@@ -83,22 +89,32 @@ def fresh_session_tip(requests: list[Request], attribution: Attribution) -> Tip
|
|
|
83
89
|
simulated.append(r)
|
|
84
90
|
continue
|
|
85
91
|
cut = min(carried_at[i], r.usage.cache_read)
|
|
92
|
+
carried_tokens += cut
|
|
86
93
|
simulated.append(replace(r, usage=replace(r.usage, cache_read=r.usage.cache_read - cut)))
|
|
87
94
|
(a_wh, a_usd), (s_wh, s_usd) = _sum(actual), _sum(simulated)
|
|
88
95
|
saved = WhRange(a_wh.low - s_wh.low, a_wh.high - s_wh.high)
|
|
89
96
|
if a_wh.mid == 0 or saved.mid / a_wh.mid < 0.02:
|
|
90
97
|
return None
|
|
91
98
|
share = saved.mid / a_wh.mid
|
|
99
|
+
tokens = sum(r.usage.ctx_len + r.usage.output for r in actual)
|
|
100
|
+
lo, hi = sorted((saved.low / a_wh.low if a_wh.low else 0.0, saved.high / a_wh.high if a_wh.high else 0.0))
|
|
101
|
+
usd = a_usd - s_usd
|
|
92
102
|
return Tip(
|
|
93
103
|
title="Start a fresh session after each commit",
|
|
94
|
-
measured=f"{
|
|
104
|
+
measured=f"{carried_tokens / tokens:.0%} of the tokens your agents processed were conversation carried over from before your last commit",
|
|
95
105
|
action="After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed",
|
|
96
106
|
saving_wh=saved,
|
|
97
|
-
saving_usd=
|
|
107
|
+
saving_usd=usd,
|
|
98
108
|
share=share,
|
|
109
|
+
saving_text=f"up to {lo:.0%}–{hi:.0%} less energy, up to {_usd_short(usd)}",
|
|
110
|
+
caveat="A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller",
|
|
99
111
|
)
|
|
100
112
|
|
|
101
113
|
|
|
114
|
+
def _usd_short(x: float) -> str:
|
|
115
|
+
return f"${x:,.0f}" if x >= 100 else f"${x:,.2f}"
|
|
116
|
+
|
|
117
|
+
|
|
102
118
|
def model_size_tip(requests: list[Request]) -> Tip | None:
|
|
103
119
|
large = [r for r in requests if model_class(r.model)[0] == "large"]
|
|
104
120
|
if not large:
|
|
@@ -31,6 +31,9 @@ def usage_cost(usage: Usage, model: str) -> float | None:
|
|
|
31
31
|
p = price_for(model)
|
|
32
32
|
if p is None:
|
|
33
33
|
return None
|
|
34
|
+
tier = p.get("long_prompt")
|
|
35
|
+
if tier and usage.ctx_len > tier["over_tokens"]: # e.g. Haiku 5.5: the whole request at the higher prices
|
|
36
|
+
p = tier
|
|
34
37
|
one_hour = min(usage.cache_write_1h, usage.cache_write)
|
|
35
38
|
return (
|
|
36
39
|
usage.fresh_in * p["input"]
|
|
@@ -20,6 +20,7 @@ Nothing here talks to the network or changes how the agent works.
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
|
+
import shlex
|
|
23
24
|
import os
|
|
24
25
|
import statistics
|
|
25
26
|
import subprocess
|
|
@@ -259,7 +260,7 @@ def status_line(data: dict, state: SessionState, usual: float | None) -> str:
|
|
|
259
260
|
else:
|
|
260
261
|
parts.append(f"ctx {_k(ctx)}")
|
|
261
262
|
if share >= CLEAR_HINT_SHARE and len(state.task) <= 3:
|
|
262
|
-
parts.append(
|
|
263
|
+
parts.append("next task? /clear")
|
|
263
264
|
five = ((data.get("rate_limits") or {}).get("five_hour") or {}).get("used_percentage")
|
|
264
265
|
if isinstance(five, (int, float)):
|
|
265
266
|
parts.append(f"5h {five:.0f}%")
|
|
@@ -270,7 +271,8 @@ def commit_note(done: dict) -> str:
|
|
|
270
271
|
msg = f"aimpg: that commit cost {_usd(done['cost'])} over {done['requests']} AI requests."
|
|
271
272
|
share = done.get("stale_share", 0.0)
|
|
272
273
|
if share >= CLEAR_HINT_SHARE:
|
|
273
|
-
msg += f" {share:.0%} of the context is from earlier work
|
|
274
|
+
msg += (f" {share:.0%} of the context is from earlier work. If the next task doesn't need it, /clear"
|
|
275
|
+
" (with a one-line summary) stops re-reading it on every request.")
|
|
274
276
|
return msg
|
|
275
277
|
|
|
276
278
|
|
|
@@ -338,9 +340,17 @@ PREVIOUS = LIVE_DIR / "previous_statusline.json"
|
|
|
338
340
|
OURS = ("aimpg statusline", "aimpg hook post-commit")
|
|
339
341
|
|
|
340
342
|
|
|
343
|
+
def _write_json(path: Path, data: dict) -> None:
|
|
344
|
+
"""Write via a temp file and rename, so Claude Code never reads a half-written settings file."""
|
|
345
|
+
tmp = path.with_name(f".{path.name}.aimpg-tmp")
|
|
346
|
+
tmp.write_text(json.dumps(data, indent=2) + "\n")
|
|
347
|
+
os.replace(tmp, path)
|
|
348
|
+
|
|
349
|
+
|
|
341
350
|
def planned_settings(current: dict, exe: str) -> dict:
|
|
342
351
|
"""The settings after install: our status line (wrapping any existing one) + a commit hook."""
|
|
343
352
|
new = json.loads(json.dumps(current))
|
|
353
|
+
exe = shlex.quote(exe) # a path with spaces or quotes must stay one argument
|
|
344
354
|
new["statusLine"] = {"type": "command", "command": f"{exe} statusline"}
|
|
345
355
|
hooks = new.setdefault("hooks", {}).setdefault("PostToolUse", [])
|
|
346
356
|
if not any(h.get("command", "").endswith("hook post-commit") for e in hooks for h in e.get("hooks", [])):
|
|
@@ -366,7 +376,7 @@ def install(settings: Path = SETTINGS, exe: str = "aimpg", *, confirm=input) ->
|
|
|
366
376
|
backup = settings.with_suffix(f".json.aimpg-backup-{int(time.time())}")
|
|
367
377
|
backup.write_text(settings.read_text())
|
|
368
378
|
settings.parent.mkdir(parents=True, exist_ok=True)
|
|
369
|
-
settings
|
|
379
|
+
_write_json(settings, new)
|
|
370
380
|
return "Installed. It shows up after the next AI message. Undo any time with `aimpg live uninstall`."
|
|
371
381
|
|
|
372
382
|
|
|
@@ -388,7 +398,7 @@ def uninstall(settings: Path = SETTINGS) -> str:
|
|
|
388
398
|
current["hooks"].pop("PostToolUse")
|
|
389
399
|
if not current["hooks"]:
|
|
390
400
|
current.pop("hooks")
|
|
391
|
-
settings
|
|
401
|
+
_write_json(settings, current)
|
|
392
402
|
PREVIOUS.unlink(missing_ok=True)
|
|
393
403
|
return "Removed. Your previous status line (if any) is back."
|
|
394
404
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2026-10-
|
|
3
|
-
"source": "https://platform.claude.com/docs/en/about-claude/pricing (Model pricing table, fetched 2026-10-
|
|
2
|
+
"version": "2026-10-11",
|
|
3
|
+
"source": "https://platform.claude.com/docs/en/about-claude/pricing (Model pricing table, fetched 2026-10-11). Claude Haiku 5.5 is priced by prompt length: a request whose prompt (all input incl. cache reads and writes) is over 100,000 tokens pays the long_prompt prices.",
|
|
4
4
|
"unit": "USD per million tokens",
|
|
5
5
|
"note": "Standard (global) API prices. For subscription users this is the API-equivalent value of the work, not a bill.",
|
|
6
6
|
"models": {
|
|
@@ -101,6 +101,21 @@
|
|
|
101
101
|
"cache_write_1h": 1.75,
|
|
102
102
|
"cache_read": 0.175,
|
|
103
103
|
"output": 14.0
|
|
104
|
+
},
|
|
105
|
+
"claude-haiku-5-5": {
|
|
106
|
+
"input": 0.1,
|
|
107
|
+
"cache_write_5m": 0.125,
|
|
108
|
+
"cache_write_1h": 0.2,
|
|
109
|
+
"cache_read": 0.01,
|
|
110
|
+
"output": 0.5,
|
|
111
|
+
"long_prompt": {
|
|
112
|
+
"over_tokens": 100000,
|
|
113
|
+
"input": 0.5,
|
|
114
|
+
"cache_write_5m": 0.625,
|
|
115
|
+
"cache_write_1h": 1.0,
|
|
116
|
+
"cache_read": 0.05,
|
|
117
|
+
"output": 2.5
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
120
|
},
|
|
106
121
|
"openai_source": "https://developers.openai.com/api/docs/pricing (Standard tier, fetched 2026-10-03). gpt-5.3-codex lists no cache-write price; writes are priced as input."
|
|
@@ -59,7 +59,7 @@ def summary(in_window, energies: list[TaskEnergy]) -> list[str]:
|
|
|
59
59
|
"YOUR AI CODING",
|
|
60
60
|
f" Energy {_fmt_wh(everything)}",
|
|
61
61
|
f" ≈ {equivalence.everyday(everything)}",
|
|
62
|
-
f" Money {_usd(usd)} API
|
|
62
|
+
f" Money {_usd(usd)} at API list prices (Anthropic's and OpenAI's; a subscription costs less"
|
|
63
63
|
+ (f"; {unknown} requests from unpriced models left out)" if unknown else ")"),
|
|
64
64
|
f" Output {len(energies)} commits made with AI help",
|
|
65
65
|
]
|
|
@@ -134,8 +134,10 @@ def render(parsed: ParseResult, attribution: Attribution, since: float, now: flo
|
|
|
134
134
|
add("")
|
|
135
135
|
add("WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)")
|
|
136
136
|
for i, t in enumerate(found, 1):
|
|
137
|
-
add(f" {i}. {t.title}: up to {t.share:.0%} less energy, {_usd(t.saving_usd)}")
|
|
137
|
+
add(f" {i}. {t.title}: {t.saving_text or f'up to {t.share:.0%} less energy, {_usd(t.saving_usd)}'}")
|
|
138
138
|
add(f" {t.measured}.")
|
|
139
|
+
if t.caveat:
|
|
140
|
+
add(f" {t.caveat}.")
|
|
139
141
|
add(f" ≈ {equivalence.everyday(t.saving_wh)}")
|
|
140
142
|
add(f" → {t.action}.")
|
|
141
143
|
if judged is not None:
|