claude-finops 0.7.2 → 0.9.0

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/README.md CHANGED
@@ -8,8 +8,10 @@ It answers, in a few clicks:
8
8
  > **What I used → what it cost → why it cost that much → whether it was efficient →
9
9
  > what is likely to happen next → and what I should change.**
10
10
 
11
- Everything runs on `127.0.0.1` with the Python standard library. No dependencies, no
12
- network calls, no data leaves the machine.
11
+ Everything runs on `127.0.0.1` with the Python standard library. No dependencies, and
12
+ no data leaves the machine. No outbound calls except a once-a-day version check
13
+ against the npm registry (disable with `--no-update-check`), and provider APIs only
14
+ if you add a key.
13
15
 
14
16
  ```bash
15
17
  npx claude-finops
@@ -97,9 +99,10 @@ set.
97
99
  - **Nothing to learn.** Every screen states its own conclusion in plain English before
98
100
  it shows you a chart, and every number carries a badge saying whether it is measured
99
101
  or estimated.
100
- - **Your data stays put.** It binds to `127.0.0.1` and makes no outbound calls. The
101
- warehouse lives in `~/.claude-finops`, so upgrading or deleting the package never
102
- touches it.
102
+ - **Your data stays put.** It binds to `127.0.0.1`. No outbound calls except a
103
+ once-a-day version check against the npm registry (disable with
104
+ `--no-update-check`), and provider APIs only if you add a key. The warehouse lives
105
+ in `~/.claude-finops`, so upgrading or deleting the package never touches it.
103
106
  - **It tells you what to change**, not just what happened — usually with a prompt you
104
107
  can paste into Claude Code.
105
108
 
@@ -115,7 +118,7 @@ carries one of four badges, and nothing is invented.
115
118
  | **Actual** | Read straight out of your transcripts: token counts, timestamps, models, effort, tool calls, file paths, session and project identity. |
116
119
  | **Estimated** | Derived. **Every dollar figure is estimated**, because Claude Code transcripts contain token counts but no billed amount. Cost = tokens × the price table in `config/pricing.json`. |
117
120
  | **Forecast** | Projected from your history. Assumes the recent pattern continues. |
118
- | **Recommendation** | A modelled opportunity. Savings estimates hold token usage constant on the alternative and do **not** model output quality. |
121
+ | **Recommendation** | A recommendation grounded in an observed share of spend. No saving is estimated. |
119
122
 
120
123
  ### Deliberately not fabricated
121
124
 
@@ -173,8 +176,9 @@ Two files, both editable without touching code. The server picks up changes to
173
176
  ### `config/pricing.json`
174
177
 
175
178
  Model prices per million tokens, kept strictly separate from usage data so the table
176
- can be updated as prices change. A model with no entry falls back to
177
- `default_model_pricing` and is flagged as such in the Model analysis view.
179
+ can be updated as prices change. A `claude-*` model with no entry is priced as
180
+ `unpriced` ($0, flagged as such in the Model analysis view) rather than silently
181
+ billed at another model's rate.
178
182
 
179
183
  ### `config/settings.json`
180
184
 
@@ -287,45 +291,6 @@ place for you to delete once you are happy.
287
291
 
288
292
  ---
289
293
 
290
- ## Running the trial, not just recommending one
291
-
292
- The evidence table ends by admitting its own limit: your prompts were never
293
- randomly assigned to models, so a category can simply have been easier on one of
294
- them. **Try it →** on any row closes that gap. It pulls prompts you actually
295
- typed in that category, re-runs them headlessly on the candidate model, and
296
- prices the result against what they cost the first time.
297
-
298
- Nothing runs on its own: a trial spends real money and drives a real agent, so
299
- it takes two clicks and shows the first-time bill before you commit. Runs happen
300
- in a scratch directory, and headless Claude cannot ask for permission — so tools
301
- that need it are denied and counted, and a task that needs your repo will look
302
- smaller than it is. The verdict says which way it went: *confirmed*, *smaller
303
- than advertised*, or *history overstated it*.
304
-
305
- ---
306
-
307
- ## Live model advice
308
-
309
- The back-test tells you what to use next time. These tell you mid-session, while
310
- you can still act on it:
311
-
312
- ```
313
- claude-finops --advise what every running session should switch to
314
- claude-finops --install-hook suggest a cheaper model as you send each prompt
315
- claude-finops --install-statusline model, context pressure and advice in your statusline
316
- ```
317
-
318
- The hook and statusline read the same evidence as the dashboard, cached for an
319
- hour, and stay quiet unless your own history shows a cheaper model doing that
320
- category of work without taking more turns. Neither can block or slow a prompt:
321
- they fail silent and always exit 0. Undo with `--uninstall-hook` /
322
- `--uninstall-statusline`; your `~/.claude/settings.json` is backed up first.
323
-
324
- The Running sessions view shows the same line per live session, with the
325
- `/model` command ready to copy.
326
-
327
- ---
328
-
329
294
  ## Privacy
330
295
 
331
296
  `~/.claude-finops/data/finops.db` and the prompt/CSV exports contain **your full prompt text**. The
@@ -2,19 +2,147 @@
2
2
  "_comment": "Model pricing in USD per 1,000,000 tokens. Kept SEPARATE from usage data so it can be updated without touching the dashboard. Any cost derived from these numbers is ESTIMATED, never actual billed cost.",
3
3
  "currency": "USD",
4
4
  "unit": "per_million_tokens",
5
- "updated": "2026-09-18",
6
- "source": "user-configurable list price table",
7
- "default_model_pricing": {
8
- "input": 3.0,
9
- "output": 15.0,
10
- "cache_write_5m": 3.75,
11
- "cache_write_1h": 6.0,
12
- "cache_read": 0.3
13
- },
5
+ "updated": "2026-09-28",
6
+ "source": "Anthropic list prices (platform.claude.com/docs/en/about-claude/pricing, fetched 2026-09-28)",
14
7
  "models": {
15
8
  "claude-opus-5": {
16
9
  "display_name": "Claude Opus 5",
17
10
  "tier": "frontier",
11
+ "input": 5.0,
12
+ "output": 25.0,
13
+ "cache_write_5m": 6.25,
14
+ "cache_write_1h": 10.0,
15
+ "cache_read": 0.5,
16
+ "context_window": 1000000,
17
+ "provider": "anthropic"
18
+ },
19
+ "claude-opus-5[fast]": {
20
+ "display_name": "Claude Opus 5 (fast mode)",
21
+ "tier": "frontier",
22
+ "input": 10.0,
23
+ "output": 50.0,
24
+ "cache_write_5m": 12.5,
25
+ "cache_write_1h": 20.0,
26
+ "cache_read": 1.0,
27
+ "context_window": 1000000,
28
+ "provider": "anthropic",
29
+ "_note": "Research-preview fast mode, 2x standard. Selected when usage.speed == 'fast'."
30
+ },
31
+ "claude-opus-5-5": {
32
+ "display_name": "Claude Opus 5.5",
33
+ "tier": "frontier",
34
+ "input": 4.0,
35
+ "output": 20.0,
36
+ "cache_write_5m": 5.0,
37
+ "cache_write_1h": 8.0,
38
+ "cache_read": 0.20,
39
+ "context_window": 1000000,
40
+ "provider": "anthropic"
41
+ },
42
+ "claude-opus-5-5[fast]": {
43
+ "display_name": "Claude Opus 5.5 (fast mode)",
44
+ "tier": "frontier",
45
+ "input": 8.0,
46
+ "output": 40.0,
47
+ "cache_write_5m": 10.0,
48
+ "cache_write_1h": 16.0,
49
+ "cache_read": 0.40,
50
+ "context_window": 1000000,
51
+ "provider": "anthropic",
52
+ "_note": "Research-preview fast mode, 2x standard. Selected when usage.speed == 'fast'."
53
+ },
54
+ "claude-sonnet-5": {
55
+ "display_name": "Claude Sonnet 5",
56
+ "tier": "balanced",
57
+ "input": 2.0,
58
+ "output": 10.0,
59
+ "cache_write_5m": 2.5,
60
+ "cache_write_1h": 4.0,
61
+ "cache_read": 0.2,
62
+ "context_window": 1000000,
63
+ "provider": "anthropic"
64
+ },
65
+ "claude-fable-5": {
66
+ "display_name": "Claude Fable 5",
67
+ "tier": "frontier",
68
+ "input": 10.0,
69
+ "output": 50.0,
70
+ "cache_write_5m": 12.5,
71
+ "cache_write_1h": 20.0,
72
+ "cache_read": 1.0,
73
+ "context_window": 1000000,
74
+ "provider": "anthropic"
75
+ },
76
+ "claude-fable-5-1": {
77
+ "display_name": "Claude Fable 5.1",
78
+ "tier": "frontier",
79
+ "input": 10.0,
80
+ "output": 50.0,
81
+ "cache_write_5m": 12.5,
82
+ "cache_write_1h": 20.0,
83
+ "cache_read": 0.25,
84
+ "context_window": 1000000,
85
+ "provider": "anthropic"
86
+ },
87
+ "claude-opus-4-8": {
88
+ "display_name": "Claude Opus 4.8",
89
+ "tier": "frontier",
90
+ "input": 5.0,
91
+ "output": 25.0,
92
+ "cache_write_5m": 6.25,
93
+ "cache_write_1h": 10.0,
94
+ "cache_read": 0.5,
95
+ "context_window": 1000000,
96
+ "provider": "anthropic"
97
+ },
98
+ "claude-opus-4-8[fast]": {
99
+ "display_name": "Claude Opus 4.8 (fast mode)",
100
+ "tier": "frontier",
101
+ "input": 10.0,
102
+ "output": 50.0,
103
+ "cache_write_5m": 12.5,
104
+ "cache_write_1h": 20.0,
105
+ "cache_read": 1.0,
106
+ "context_window": 1000000,
107
+ "provider": "anthropic",
108
+ "_note": "Research-preview fast mode, 2x standard. Selected when usage.speed == 'fast'."
109
+ },
110
+ "claude-opus-4-7": {
111
+ "display_name": "Claude Opus 4.7",
112
+ "tier": "frontier",
113
+ "input": 5.0,
114
+ "output": 25.0,
115
+ "cache_write_5m": 6.25,
116
+ "cache_write_1h": 10.0,
117
+ "cache_read": 0.5,
118
+ "context_window": 1000000,
119
+ "provider": "anthropic"
120
+ },
121
+ "claude-opus-4-6": {
122
+ "display_name": "Claude Opus 4.6",
123
+ "tier": "frontier",
124
+ "input": 5.0,
125
+ "output": 25.0,
126
+ "cache_write_5m": 6.25,
127
+ "cache_write_1h": 10.0,
128
+ "cache_read": 0.5,
129
+ "context_window": 1000000,
130
+ "provider": "anthropic"
131
+ },
132
+ "claude-opus-4-5": {
133
+ "display_name": "Claude Opus 4.5",
134
+ "tier": "frontier",
135
+ "input": 5.0,
136
+ "output": 25.0,
137
+ "cache_write_5m": 6.25,
138
+ "cache_write_1h": 10.0,
139
+ "cache_read": 0.5,
140
+ "context_window": 200000,
141
+ "provider": "anthropic"
142
+ },
143
+ "claude-opus-4-1": {
144
+ "display_name": "Claude Opus 4.1",
145
+ "tier": "frontier",
18
146
  "input": 15.0,
19
147
  "output": 75.0,
20
148
  "cache_write_5m": 18.75,
@@ -23,30 +151,30 @@
23
151
  "context_window": 200000,
24
152
  "provider": "anthropic"
25
153
  },
26
- "claude-opus-5[1m]": {
27
- "display_name": "Claude Opus 5 (1M context)",
154
+ "claude-opus-4": {
155
+ "display_name": "Claude Opus 4",
28
156
  "tier": "frontier",
29
- "input": 22.5,
30
- "output": 112.5,
31
- "cache_write_5m": 28.125,
32
- "cache_write_1h": 45.0,
33
- "cache_read": 2.25,
34
- "context_window": 1000000,
157
+ "input": 15.0,
158
+ "output": 75.0,
159
+ "cache_write_5m": 18.75,
160
+ "cache_write_1h": 30.0,
161
+ "cache_read": 1.5,
162
+ "context_window": 200000,
35
163
  "provider": "anthropic"
36
164
  },
37
- "claude-sonnet-5": {
38
- "display_name": "Claude Sonnet 5",
165
+ "claude-sonnet-4-6": {
166
+ "display_name": "Claude Sonnet 4.6",
39
167
  "tier": "balanced",
40
168
  "input": 3.0,
41
169
  "output": 15.0,
42
170
  "cache_write_5m": 3.75,
43
171
  "cache_write_1h": 6.0,
44
172
  "cache_read": 0.3,
45
- "context_window": 200000,
173
+ "context_window": 1000000,
46
174
  "provider": "anthropic"
47
175
  },
48
- "claude-fable-5": {
49
- "display_name": "Claude Fable 5",
176
+ "claude-sonnet-4-5": {
177
+ "display_name": "Claude Sonnet 4.5",
50
178
  "tier": "balanced",
51
179
  "input": 3.0,
52
180
  "output": 15.0,
@@ -56,8 +184,8 @@
56
184
  "context_window": 200000,
57
185
  "provider": "anthropic"
58
186
  },
59
- "claude-fable-5-1": {
60
- "display_name": "Claude Fable 5.1",
187
+ "claude-sonnet-4": {
188
+ "display_name": "Claude Sonnet 4",
61
189
  "tier": "balanced",
62
190
  "input": 3.0,
63
191
  "output": 15.0,
@@ -67,6 +195,17 @@
67
195
  "context_window": 200000,
68
196
  "provider": "anthropic"
69
197
  },
198
+ "claude-3-5-haiku": {
199
+ "display_name": "Claude Haiku 3.5",
200
+ "tier": "economy",
201
+ "input": 0.8,
202
+ "output": 4.0,
203
+ "cache_write_5m": 1.0,
204
+ "cache_write_1h": 1.6,
205
+ "cache_read": 0.08,
206
+ "context_window": 200000,
207
+ "provider": "anthropic"
208
+ },
70
209
  "claude-haiku-4-5-20251001": {
71
210
  "display_name": "Claude Haiku 4.5",
72
211
  "tier": "economy",
@@ -225,6 +364,9 @@
225
364
  "_note": "Cursor bills by subscription and doesn't record the model locally: no cost is estimated"
226
365
  }
227
366
  },
367
+ "aliases": {
368
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
369
+ },
228
370
  "_sources": {
229
371
  "openai": "https://developers.openai.com/api/docs/pricing (standard tier, 2026-09-18)",
230
372
  "google": "https://ai.google.dev/gemini-api/docs/pricing (paid tier, 2026-09-18)"
@@ -41,17 +41,23 @@
41
41
  "frontier"
42
42
  ],
43
43
  "low_output_ratio_vs_median": 0.4,
44
+ "tool_loop_calls": 40,
45
+ "poor_cache_min_writes": 500000,
44
46
  "_low_output_note": "Sessions are flagged when their output/billable ratio falls below this fraction of YOUR median session ratio, rather than an absolute number, because a healthy ratio depends on how agentic your workload is."
45
47
  },
48
+ "hygiene": {
49
+ "_comment": "Context sizes (prompt-side tokens) the hygiene view reports spend above. Observed shares only; no saving is estimated from them.",
50
+ "context_thresholds": [100000, 150000]
51
+ },
46
52
  "anomaly": {
53
+ "_comment": "session_ratio is measured against the MEDIAN session, not the mean: session sizes are right-skewed enough that a mean lets the outliers inflate their own baseline. Session token counts are heavy-tailed, so max_session_outliers, not the ratio, is what keeps the list short and leaves room for the other anomaly types. daily_robust_z is a threshold on a median/MAD robust z-score (MAD scaled by 1.4826 to estimate a stdev), not the plain daily_zscore: unpriced $0 days from agents with no pricing data would otherwise pollute a mean/stdev baseline.",
47
54
  "daily_zscore": 2.0,
48
55
  "daily_ratio": 2.0,
49
- "session_ratio": 3.0
56
+ "daily_robust_z": 3.5,
57
+ "session_ratio": 20.0,
58
+ "max_session_outliers": 5
50
59
  },
51
60
  "scorecard": {
52
- "_comment": "Reference points the 0-100 dimension scores are graded against. Derived from your own data by default; override to hold yourself to a different bar.",
53
- "target_output_ratio": 0.0088,
54
- "target_cost_per_1k_output_usd": 0.3,
55
- "frontier_cost_share_allowance_pct": 40
61
+ "_comment": "The scorecard has no targets to configure: its three dimensions (Context share, Cache break-even, Budget adherence) are each measured directly from your transcripts, except budget adherence which needs a budget configured above."
56
62
  }
57
63
  }
package/finops/actions.py CHANGED
@@ -18,7 +18,7 @@ import urllib.request
18
18
  import uuid
19
19
  from collections import defaultdict
20
20
 
21
- from .analytics import DB_PATH, ROOT
21
+ from .paths import DB_PATH, ROOT
22
22
  from .etl import DEFAULT_SOURCE, Loader
23
23
 
24
24
  HOME = os.path.expanduser("~")
package/finops/agents.py CHANGED
@@ -33,7 +33,8 @@ def _cursor_state_db():
33
33
 
34
34
  AGENTS = {
35
35
  "claude": {"name": "Claude Code", "data": "full",
36
- "note": "Tokens, model, cost, prompts and tool calls per request."},
36
+ "note": "Tokens, model, cost, prompts and tool calls per request. "
37
+ "Includes Claude desktop app (Cowork) sessions when present."},
37
38
  "codex": {"name": "Codex", "data": "tokens",
38
39
  "paths": [os.path.join(HOME, ".codex", "sessions")],
39
40
  "note": "Tokens and model per turn. Cost estimated at OpenAI API list prices; "