mycroftcompute 0.1.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.
- mycroftcompute-0.1.0/.gitignore +13 -0
- mycroftcompute-0.1.0/LICENSE +21 -0
- mycroftcompute-0.1.0/PKG-INFO +156 -0
- mycroftcompute-0.1.0/README.md +133 -0
- mycroftcompute-0.1.0/pyproject.toml +72 -0
- mycroftcompute-0.1.0/src/mycroftcompute/__init__.py +0 -0
- mycroftcompute-0.1.0/src/mycroftcompute/__main__.py +282 -0
- mycroftcompute-0.1.0/src/mycroftcompute/analysis/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/analysis/costing.py +168 -0
- mycroftcompute-0.1.0/src/mycroftcompute/analysis/findings.py +576 -0
- mycroftcompute-0.1.0/src/mycroftcompute/analysis/team.py +393 -0
- mycroftcompute-0.1.0/src/mycroftcompute/economics/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/economics/caching.py +328 -0
- mycroftcompute-0.1.0/src/mycroftcompute/economics/prices.py +444 -0
- mycroftcompute-0.1.0/src/mycroftcompute/parsers/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/parsers/claude_code.py +219 -0
- mycroftcompute-0.1.0/src/mycroftcompute/parsers/model.py +176 -0
- mycroftcompute-0.1.0/src/mycroftcompute/report/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/report/readiness.py +211 -0
- mycroftcompute-0.1.0/src/mycroftcompute/report/text.py +512 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sample.py +173 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/base.py +35 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/datadog.py +766 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/grafana.py +580 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/merging.py +156 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/metrics_names.py +217 -0
- mycroftcompute-0.1.0/src/mycroftcompute/sources/records.py +172 -0
- mycroftcompute-0.1.0/src/mycroftcompute/store/__init__.py +1 -0
- mycroftcompute-0.1.0/src/mycroftcompute/store/history.py +456 -0
- mycroftcompute-0.1.0/src/mycroftcompute/store/schema.py +86 -0
- mycroftcompute-0.1.0/src/mycroftcompute/vendors.py +17 -0
- mycroftcompute-0.1.0/tests/test_caching.py +384 -0
- mycroftcompute-0.1.0/tests/test_claude_code.py +193 -0
- mycroftcompute-0.1.0/tests/test_cli.py +229 -0
- mycroftcompute-0.1.0/tests/test_datadog_source.py +510 -0
- mycroftcompute-0.1.0/tests/test_findings.py +497 -0
- mycroftcompute-0.1.0/tests/test_grafana_source.py +560 -0
- mycroftcompute-0.1.0/tests/test_history.py +241 -0
- mycroftcompute-0.1.0/tests/test_metric_families.py +244 -0
- mycroftcompute-0.1.0/tests/test_readiness.py +165 -0
- mycroftcompute-0.1.0/tests/test_report.py +268 -0
- mycroftcompute-0.1.0/tests/test_team.py +470 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aditya Negi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mycroftcompute
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Where your Claude Code money went, priced with the prompt cache counted, and which changes would actually pay.
|
|
5
|
+
Project-URL: Homepage, https://www.mycroftcompute.com
|
|
6
|
+
Project-URL: Repository, https://github.com/negi6711/mycroftcompute
|
|
7
|
+
Project-URL: Issues, https://github.com/negi6711/mycroftcompute/issues
|
|
8
|
+
Author: Aditya Negi
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,claude-code,cost,datadog,finops,grafana,llm,opentelemetry,prompt-caching
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# mycroftcompute
|
|
25
|
+
|
|
26
|
+
**Where your Claude Code money went, priced with the prompt cache counted, and which changes would
|
|
27
|
+
actually pay.**
|
|
28
|
+
|
|
29
|
+
[ccusage](https://github.com/ccusage/ccusage) tells you what Claude Code cost you. mycroftcompute
|
|
30
|
+
tells you what's worth changing, and what that change is worth at most.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uvx mycroftcompute # read your own Claude Code history (~/.claude/projects)
|
|
34
|
+
uvx mycroftcompute --sample # or see the report for an invented 24-developer team
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Python 3.12+, no dependencies. Nothing leaves your machine. `pipx install mycroftcompute` works
|
|
38
|
+
too.
|
|
39
|
+
|
|
40
|
+
## What a finding looks like
|
|
41
|
+
|
|
42
|
+
From `--sample`:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
FINDING 3 - 23.7% of your bill went on writing the prompt cache
|
|
46
|
+
------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
What your metrics backend shows
|
|
49
|
+
- 608,889,600 tokens were written to the prompt cache, costing $3,069.00 at the
|
|
50
|
+
five-minute write rate - 23.7% of the $12,928.43 priced here. At the one-hour
|
|
51
|
+
rate the same writes would cost $4,910.40.
|
|
52
|
+
- 15,713,280,000 tokens were read back from cache, about 25.8 reads for every
|
|
53
|
+
token written.
|
|
54
|
+
- Read from cache instead of written, the same tokens would have cost $245.52.
|
|
55
|
+
|
|
56
|
+
At most $2,823.48 over this period
|
|
57
|
+
This is the gap between what these tokens cost and what they would cost under
|
|
58
|
+
the cheaper option. It is a ceiling, not a saving - see below.
|
|
59
|
+
|
|
60
|
+
What your metrics backend cannot tell you
|
|
61
|
+
- How many of these writes were a warm prefix written again - after a new session,
|
|
62
|
+
a compaction, a model switch, a change to the tools or system prompt, or a pause
|
|
63
|
+
longer than the cache lives - and how many were new context that had to be
|
|
64
|
+
written once. Only the first kind can be avoided ...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Every finding has the same three parts: what the data shows, the most a change could be worth,
|
|
68
|
+
and what the data cannot tell you.
|
|
69
|
+
|
|
70
|
+
## Why the cache changes the answer
|
|
71
|
+
|
|
72
|
+
Claude Code already serves most of its input from the prompt cache, typically above 90%. So
|
|
73
|
+
"turn caching on" is not the saving. What costs money is **writing** the cache: each write is
|
|
74
|
+
billed above list price (1.25x for five minutes, 2x for an hour), and a model switch throws the
|
|
75
|
+
warm cache away.
|
|
76
|
+
|
|
77
|
+
That is why a cheaper model is not always cheaper. On a typical coding-agent step (about 141,000
|
|
78
|
+
cached prompt tokens and 1,000 fresh ones):
|
|
79
|
+
|
|
80
|
+
| Decision | Input cost per step |
|
|
81
|
+
|---|---|
|
|
82
|
+
| Stay on Sonnet 4.6, cache warm | $0.045 |
|
|
83
|
+
| First request after moving to Haiku 4.5 | $0.177 - **3.9x worse** |
|
|
84
|
+
| Every request after that, Haiku's cache warm | $0.015 - 3x better |
|
|
85
|
+
| Alternating between the two | $0.426 - **9.4x worse** |
|
|
86
|
+
|
|
87
|
+
The rule that falls out: choose the model when a session starts, then stay. Every router and
|
|
88
|
+
dashboard compares list prices; mycroftcompute counts the cache before it suggests a model change.
|
|
89
|
+
|
|
90
|
+
## What it finds
|
|
91
|
+
|
|
92
|
+
| Finding | Fires when |
|
|
93
|
+
|---|---|
|
|
94
|
+
| **Cache writes** | Writing the cache is 20% or more of the bill. Ceiling: those tokens priced as reads instead. |
|
|
95
|
+
| **Model switch, net of cache** | A cheaper model would cost less once its own cache is warm. |
|
|
96
|
+
| **Housekeeping on an expensive model** | The agent's own background calls (`query_source = auxiliary`) run on a flagship model (team data: the label comes from telemetry). |
|
|
97
|
+
| **Uncached input** | Less than half of input comes from cache, which is unusual for Claude Code. |
|
|
98
|
+
| **Developer concentration** | A few people account for most of the spend (team data, 4+ developers). |
|
|
99
|
+
| **Continuous usage** | Spend doesn't drop at weekends, so something automated is driving it (team data). |
|
|
100
|
+
|
|
101
|
+
Run it again later with `--db history.db` and the report opens with **since last time**: what
|
|
102
|
+
appeared, what stopped firing, and how each ceiling moved, comparing equal windows.
|
|
103
|
+
|
|
104
|
+
## For a team: Grafana or Datadog
|
|
105
|
+
|
|
106
|
+
If your team already sends Claude Code's telemetry to Grafana Cloud (or any Prometheus) or
|
|
107
|
+
Datadog, mycroftcompute reads it with one read-only credential:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
export MYCROFT_METRICS_TOKEN=glc_... # Cloud Access Policy token, metrics:read
|
|
111
|
+
mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --discover
|
|
112
|
+
mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --days 14
|
|
113
|
+
|
|
114
|
+
export MYCROFT_METRICS_TOKEN=... # Datadog API key
|
|
115
|
+
export MYCROFT_DATADOG_APP_KEY=... # Application key: metrics_read, timeseries_query
|
|
116
|
+
mycroftcompute --datadog us1 --days 14
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`--discover` checks the connection and lists what the telemetry can and cannot support before
|
|
120
|
+
reading any usage. Setup, including the two settings that silently lose every number, is in
|
|
121
|
+
[docs/team-backends.md](docs/team-backends.md). For a two-minute check you can run inside Grafana
|
|
122
|
+
or Datadog without installing anything, see [docs/cache-check.md](docs/cache-check.md).
|
|
123
|
+
|
|
124
|
+
Both backends were verified against live Claude Code telemetry in September 2026.
|
|
125
|
+
|
|
126
|
+
## How the numbers are made
|
|
127
|
+
|
|
128
|
+
- **List prices**, checked by hand against each vendor's own pricing page, for 19 models from
|
|
129
|
+
Anthropic, OpenAI and Google, with cache read and write multipliers and per-model cache minimums.
|
|
130
|
+
Prices were last checked on **24 September 2026**; when the table is more than 31 days old the
|
|
131
|
+
report says so. Negotiated or batch rates are lower, so every figure is correspondingly high.
|
|
132
|
+
- **Ceilings, not savings.** Each figure is the most a change could be worth over the period.
|
|
133
|
+
Ceilings overlap, so they are never added up.
|
|
134
|
+
- **Your own transcripts** record each cache write's lifetime, so local reports price five-minute
|
|
135
|
+
and one-hour writes exactly. **Metrics backends** don't, so team reports price every write at
|
|
136
|
+
the cheaper five-minute rate and say so.
|
|
137
|
+
- **Read-only.** It never reads prompts, responses or code from a backend, and never changes
|
|
138
|
+
anything. Credentials come from the environment and are only used to query.
|
|
139
|
+
|
|
140
|
+
## Status
|
|
141
|
+
|
|
142
|
+
A personal project. It started as the engine of a product for platform teams, which I stopped in
|
|
143
|
+
October 2026; the analysis was worth keeping. The price table will age between updates, and the
|
|
144
|
+
report tells you when it has.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
uv sync
|
|
150
|
+
uv run pytest
|
|
151
|
+
uv run ruff check src tests && uv run mypy
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# mycroftcompute
|
|
2
|
+
|
|
3
|
+
**Where your Claude Code money went, priced with the prompt cache counted, and which changes would
|
|
4
|
+
actually pay.**
|
|
5
|
+
|
|
6
|
+
[ccusage](https://github.com/ccusage/ccusage) tells you what Claude Code cost you. mycroftcompute
|
|
7
|
+
tells you what's worth changing, and what that change is worth at most.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
uvx mycroftcompute # read your own Claude Code history (~/.claude/projects)
|
|
11
|
+
uvx mycroftcompute --sample # or see the report for an invented 24-developer team
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python 3.12+, no dependencies. Nothing leaves your machine. `pipx install mycroftcompute` works
|
|
15
|
+
too.
|
|
16
|
+
|
|
17
|
+
## What a finding looks like
|
|
18
|
+
|
|
19
|
+
From `--sample`:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
FINDING 3 - 23.7% of your bill went on writing the prompt cache
|
|
23
|
+
------------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
What your metrics backend shows
|
|
26
|
+
- 608,889,600 tokens were written to the prompt cache, costing $3,069.00 at the
|
|
27
|
+
five-minute write rate - 23.7% of the $12,928.43 priced here. At the one-hour
|
|
28
|
+
rate the same writes would cost $4,910.40.
|
|
29
|
+
- 15,713,280,000 tokens were read back from cache, about 25.8 reads for every
|
|
30
|
+
token written.
|
|
31
|
+
- Read from cache instead of written, the same tokens would have cost $245.52.
|
|
32
|
+
|
|
33
|
+
At most $2,823.48 over this period
|
|
34
|
+
This is the gap between what these tokens cost and what they would cost under
|
|
35
|
+
the cheaper option. It is a ceiling, not a saving - see below.
|
|
36
|
+
|
|
37
|
+
What your metrics backend cannot tell you
|
|
38
|
+
- How many of these writes were a warm prefix written again - after a new session,
|
|
39
|
+
a compaction, a model switch, a change to the tools or system prompt, or a pause
|
|
40
|
+
longer than the cache lives - and how many were new context that had to be
|
|
41
|
+
written once. Only the first kind can be avoided ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every finding has the same three parts: what the data shows, the most a change could be worth,
|
|
45
|
+
and what the data cannot tell you.
|
|
46
|
+
|
|
47
|
+
## Why the cache changes the answer
|
|
48
|
+
|
|
49
|
+
Claude Code already serves most of its input from the prompt cache, typically above 90%. So
|
|
50
|
+
"turn caching on" is not the saving. What costs money is **writing** the cache: each write is
|
|
51
|
+
billed above list price (1.25x for five minutes, 2x for an hour), and a model switch throws the
|
|
52
|
+
warm cache away.
|
|
53
|
+
|
|
54
|
+
That is why a cheaper model is not always cheaper. On a typical coding-agent step (about 141,000
|
|
55
|
+
cached prompt tokens and 1,000 fresh ones):
|
|
56
|
+
|
|
57
|
+
| Decision | Input cost per step |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Stay on Sonnet 4.6, cache warm | $0.045 |
|
|
60
|
+
| First request after moving to Haiku 4.5 | $0.177 - **3.9x worse** |
|
|
61
|
+
| Every request after that, Haiku's cache warm | $0.015 - 3x better |
|
|
62
|
+
| Alternating between the two | $0.426 - **9.4x worse** |
|
|
63
|
+
|
|
64
|
+
The rule that falls out: choose the model when a session starts, then stay. Every router and
|
|
65
|
+
dashboard compares list prices; mycroftcompute counts the cache before it suggests a model change.
|
|
66
|
+
|
|
67
|
+
## What it finds
|
|
68
|
+
|
|
69
|
+
| Finding | Fires when |
|
|
70
|
+
|---|---|
|
|
71
|
+
| **Cache writes** | Writing the cache is 20% or more of the bill. Ceiling: those tokens priced as reads instead. |
|
|
72
|
+
| **Model switch, net of cache** | A cheaper model would cost less once its own cache is warm. |
|
|
73
|
+
| **Housekeeping on an expensive model** | The agent's own background calls (`query_source = auxiliary`) run on a flagship model (team data: the label comes from telemetry). |
|
|
74
|
+
| **Uncached input** | Less than half of input comes from cache, which is unusual for Claude Code. |
|
|
75
|
+
| **Developer concentration** | A few people account for most of the spend (team data, 4+ developers). |
|
|
76
|
+
| **Continuous usage** | Spend doesn't drop at weekends, so something automated is driving it (team data). |
|
|
77
|
+
|
|
78
|
+
Run it again later with `--db history.db` and the report opens with **since last time**: what
|
|
79
|
+
appeared, what stopped firing, and how each ceiling moved, comparing equal windows.
|
|
80
|
+
|
|
81
|
+
## For a team: Grafana or Datadog
|
|
82
|
+
|
|
83
|
+
If your team already sends Claude Code's telemetry to Grafana Cloud (or any Prometheus) or
|
|
84
|
+
Datadog, mycroftcompute reads it with one read-only credential:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
export MYCROFT_METRICS_TOKEN=glc_... # Cloud Access Policy token, metrics:read
|
|
88
|
+
mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --discover
|
|
89
|
+
mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --days 14
|
|
90
|
+
|
|
91
|
+
export MYCROFT_METRICS_TOKEN=... # Datadog API key
|
|
92
|
+
export MYCROFT_DATADOG_APP_KEY=... # Application key: metrics_read, timeseries_query
|
|
93
|
+
mycroftcompute --datadog us1 --days 14
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`--discover` checks the connection and lists what the telemetry can and cannot support before
|
|
97
|
+
reading any usage. Setup, including the two settings that silently lose every number, is in
|
|
98
|
+
[docs/team-backends.md](docs/team-backends.md). For a two-minute check you can run inside Grafana
|
|
99
|
+
or Datadog without installing anything, see [docs/cache-check.md](docs/cache-check.md).
|
|
100
|
+
|
|
101
|
+
Both backends were verified against live Claude Code telemetry in September 2026.
|
|
102
|
+
|
|
103
|
+
## How the numbers are made
|
|
104
|
+
|
|
105
|
+
- **List prices**, checked by hand against each vendor's own pricing page, for 19 models from
|
|
106
|
+
Anthropic, OpenAI and Google, with cache read and write multipliers and per-model cache minimums.
|
|
107
|
+
Prices were last checked on **24 September 2026**; when the table is more than 31 days old the
|
|
108
|
+
report says so. Negotiated or batch rates are lower, so every figure is correspondingly high.
|
|
109
|
+
- **Ceilings, not savings.** Each figure is the most a change could be worth over the period.
|
|
110
|
+
Ceilings overlap, so they are never added up.
|
|
111
|
+
- **Your own transcripts** record each cache write's lifetime, so local reports price five-minute
|
|
112
|
+
and one-hour writes exactly. **Metrics backends** don't, so team reports price every write at
|
|
113
|
+
the cheaper five-minute rate and say so.
|
|
114
|
+
- **Read-only.** It never reads prompts, responses or code from a backend, and never changes
|
|
115
|
+
anything. Credentials come from the environment and are only used to query.
|
|
116
|
+
|
|
117
|
+
## Status
|
|
118
|
+
|
|
119
|
+
A personal project. It started as the engine of a product for platform teams, which I stopped in
|
|
120
|
+
October 2026; the analysis was worth keeping. The price table will age between updates, and the
|
|
121
|
+
report tells you when it has.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
uv sync
|
|
127
|
+
uv run pytest
|
|
128
|
+
uv run ruff check src tests && uv run mypy
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mycroftcompute"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Where your Claude Code money went, priced with the prompt cache counted, and which changes would actually pay."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Aditya Negi" }]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = []
|
|
11
|
+
keywords = ["claude-code", "anthropic", "llm", "cost", "prompt-caching", "grafana", "datadog", "opentelemetry", "finops"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development",
|
|
21
|
+
"Topic :: System :: Monitoring",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://www.mycroftcompute.com"
|
|
26
|
+
Repository = "https://github.com/negi6711/mycroftcompute"
|
|
27
|
+
Issues = "https://github.com/negi6711/mycroftcompute/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
mycroftcompute = "mycroftcompute.__main__:main"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3",
|
|
35
|
+
"ruff>=0.8",
|
|
36
|
+
"mypy>=1.13",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling>=1.27"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/mycroftcompute"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py312"
|
|
52
|
+
src = ["src", "tests"]
|
|
53
|
+
extend-exclude = ["site-src"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "UP", "B", "BLE", "SIM", "N", "ASYNC", "S", "RUF"]
|
|
57
|
+
ignore = ["S101", "E501"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
# Fake credentials in tests are the point of the test, not a leak.
|
|
61
|
+
"tests/**" = ["S105", "S106"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.12"
|
|
65
|
+
strict = true
|
|
66
|
+
warn_unreachable = true
|
|
67
|
+
mypy_path = "src"
|
|
68
|
+
files = ["src", "tests"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
pythonpath = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Read Claude Code usage, price it with the prompt cache counted, and print what is worth changing.
|
|
2
|
+
|
|
3
|
+
mycroftcompute your own Claude Code history (~/.claude/projects)
|
|
4
|
+
mycroftcompute --sample an invented 24-developer team, to see the report
|
|
5
|
+
mycroftcompute --grafana <url> --user <instance-id>
|
|
6
|
+
mycroftcompute --datadog eu1
|
|
7
|
+
|
|
8
|
+
Credentials are read from the environment by default, never written anywhere, and used only to
|
|
9
|
+
query. Nothing is uploaded, and nothing is stored unless you pass --db.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from datetime import date, timedelta
|
|
19
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from mycroftcompute import sample
|
|
23
|
+
from mycroftcompute.analysis.findings import analyse
|
|
24
|
+
from mycroftcompute.economics.prices import StalePriceTableError, verify_prices
|
|
25
|
+
from mycroftcompute.parsers.claude_code import parse_claude_code
|
|
26
|
+
from mycroftcompute.parsers.model import ParsedUsage, ParseError
|
|
27
|
+
from mycroftcompute.report.readiness import render_readiness
|
|
28
|
+
from mycroftcompute.report.text import render
|
|
29
|
+
from mycroftcompute.sources.base import SourceError
|
|
30
|
+
from mycroftcompute.sources.datadog import SITES, DatadogEndpoint, DatadogSource
|
|
31
|
+
from mycroftcompute.sources.grafana import GrafanaSource, PrometheusEndpoint
|
|
32
|
+
from mycroftcompute.store.history import (
|
|
33
|
+
Comparison,
|
|
34
|
+
compare,
|
|
35
|
+
load_pull,
|
|
36
|
+
open_store,
|
|
37
|
+
save,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
PROG = "mycroftcompute"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _version() -> str:
|
|
44
|
+
try:
|
|
45
|
+
return version(PROG)
|
|
46
|
+
except PackageNotFoundError:
|
|
47
|
+
return "unknown"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def default_transcripts() -> Path:
|
|
51
|
+
"""Where Claude Code keeps its session files on this machine.
|
|
52
|
+
|
|
53
|
+
`CLAUDE_CONFIG_DIR` moves the whole ~/.claude directory, so it moves the transcripts too.
|
|
54
|
+
"""
|
|
55
|
+
config = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
56
|
+
base = Path(config).expanduser() if config else Path.home() / ".claude"
|
|
57
|
+
return base / "projects"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: list[str] | None = None) -> int:
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog=PROG,
|
|
63
|
+
description=(
|
|
64
|
+
"Where your Claude Code money went, priced with the prompt cache counted, and which "
|
|
65
|
+
"changes would actually pay. With no arguments it reads your own Claude Code history."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"path",
|
|
70
|
+
type=Path,
|
|
71
|
+
nargs="?",
|
|
72
|
+
help=(
|
|
73
|
+
"a directory of Claude Code transcripts. Defaults to ~/.claude/projects "
|
|
74
|
+
"(or $CLAUDE_CONFIG_DIR/projects)."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--sample",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="print the report for an invented 24-developer team, to see what the report shows",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument("--version", action="version", version=f"{PROG} {_version()}")
|
|
83
|
+
metrics = parser.add_argument_group(
|
|
84
|
+
"reading a team's metrics backend",
|
|
85
|
+
"For a team that already sends Claude Code telemetry to Grafana or Datadog. Nothing is "
|
|
86
|
+
"uploaded; the backend is queried read-only.",
|
|
87
|
+
)
|
|
88
|
+
metrics.add_argument(
|
|
89
|
+
"--grafana",
|
|
90
|
+
metavar="URL",
|
|
91
|
+
help=(
|
|
92
|
+
"Prometheus-compatible query endpoint - Grafana Cloud, Grafana OSS or plain "
|
|
93
|
+
"Prometheus, e.g. https://prometheus-prod-01.grafana.net/api/prom"
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
metrics.add_argument(
|
|
97
|
+
"--user",
|
|
98
|
+
metavar="ID",
|
|
99
|
+
help="Grafana Cloud metrics instance ID. Omit for an unauthenticated local Prometheus.",
|
|
100
|
+
)
|
|
101
|
+
metrics.add_argument(
|
|
102
|
+
"--token",
|
|
103
|
+
metavar="TOKEN",
|
|
104
|
+
help=(
|
|
105
|
+
"Cloud Access Policy token with the metrics:read scope. An ordinary Grafana API key "
|
|
106
|
+
"or service account token will not work for querying metrics. "
|
|
107
|
+
"Reads MYCROFT_METRICS_TOKEN if omitted, so it need not appear in shell history."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
metrics.add_argument(
|
|
111
|
+
"--datadog",
|
|
112
|
+
metavar="SITE",
|
|
113
|
+
nargs="?",
|
|
114
|
+
const="us1",
|
|
115
|
+
help=(
|
|
116
|
+
"read from Datadog instead. Takes the site your org is on ("
|
|
117
|
+
+ ", ".join(sorted(SITES))
|
|
118
|
+
+ "; default us1). If you pick the wrong one we will tell you which it is. "
|
|
119
|
+
"Needs --token as the API key and --app-key as the Application key."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
metrics.add_argument(
|
|
123
|
+
"--app-key",
|
|
124
|
+
metavar="KEY",
|
|
125
|
+
help=(
|
|
126
|
+
"Datadog Application key, scoped to metrics_read and timeseries_query. Reads "
|
|
127
|
+
"MYCROFT_DATADOG_APP_KEY if omitted. The API key goes in --token."
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
metrics.add_argument(
|
|
131
|
+
"--days",
|
|
132
|
+
type=int,
|
|
133
|
+
default=30,
|
|
134
|
+
metavar="N",
|
|
135
|
+
help="how many days back to read (default 30)",
|
|
136
|
+
)
|
|
137
|
+
metrics.add_argument(
|
|
138
|
+
"--discover",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help=(
|
|
141
|
+
"check the connection and stop: what we can see, what we cannot, and what each "
|
|
142
|
+
"blind spot costs. Run this first on a backend nobody has seen before."
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--db",
|
|
147
|
+
type=Path,
|
|
148
|
+
metavar="PATH",
|
|
149
|
+
help=(
|
|
150
|
+
"keep a history at this path and compare this read with the last one. Re-reading "
|
|
151
|
+
"overlapping days corrects them rather than double-counting."
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--today",
|
|
156
|
+
type=date.fromisoformat,
|
|
157
|
+
default=date.today(),
|
|
158
|
+
help="override today's date (used to judge how old the price table is)",
|
|
159
|
+
)
|
|
160
|
+
args = parser.parse_args(argv)
|
|
161
|
+
|
|
162
|
+
if args.grafana and args.datadog:
|
|
163
|
+
parser.error("read one backend at a time: --grafana or --datadog, not both")
|
|
164
|
+
|
|
165
|
+
if args.sample:
|
|
166
|
+
return _sample()
|
|
167
|
+
|
|
168
|
+
if args.grafana or args.datadog:
|
|
169
|
+
return _from_metrics(args)
|
|
170
|
+
|
|
171
|
+
path = args.path or default_transcripts()
|
|
172
|
+
if not path.exists():
|
|
173
|
+
print(
|
|
174
|
+
f"{PROG}: no Claude Code transcripts at {path}. Claude Code keeps them in "
|
|
175
|
+
"~/.claude/projects once you have used it; to read a team's Grafana or Datadog "
|
|
176
|
+
"instead, see --help. To see a report without any data, run with --sample.",
|
|
177
|
+
file=sys.stderr,
|
|
178
|
+
)
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
parsed = parse_claude_code(path)
|
|
183
|
+
except ParseError as error:
|
|
184
|
+
print(f"{PROG}: {error.message}", file=sys.stderr)
|
|
185
|
+
return 1
|
|
186
|
+
return _emit(parsed, args.today, db=args.db)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _sample() -> int:
|
|
190
|
+
"""The invented team's second month, with its comparison against the first."""
|
|
191
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
192
|
+
path = Path(tmp) / "sample.db"
|
|
193
|
+
_first, second = sample.write_history(path)
|
|
194
|
+
connection = open_store(path)
|
|
195
|
+
try:
|
|
196
|
+
parsed = load_pull(connection, second)
|
|
197
|
+
since = compare(connection, second)
|
|
198
|
+
finally:
|
|
199
|
+
connection.close()
|
|
200
|
+
print(render(parsed, analyse(parsed), today=sample.GENERATED_ON, since=since))
|
|
201
|
+
print("\n(An invented team. The arithmetic is real.)")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _source(args: argparse.Namespace) -> GrafanaSource | DatadogSource:
|
|
206
|
+
"""Whichever backend was asked for.
|
|
207
|
+
|
|
208
|
+
Credentials come from the environment by default. A key on the command line ends up in shell
|
|
209
|
+
history and in `ps` output, where it outlives the session and is readable by other users, so
|
|
210
|
+
the environment variable is the documented path and the flag is the convenience.
|
|
211
|
+
"""
|
|
212
|
+
token = args.token or os.environ.get("MYCROFT_METRICS_TOKEN")
|
|
213
|
+
if args.datadog:
|
|
214
|
+
app_key = args.app_key or os.environ.get("MYCROFT_DATADOG_APP_KEY")
|
|
215
|
+
if not token or not app_key:
|
|
216
|
+
raise SourceError(
|
|
217
|
+
"missing_credentials",
|
|
218
|
+
"Datadog needs two keys: an API key (--token or MYCROFT_METRICS_TOKEN) and an "
|
|
219
|
+
"Application key (--app-key or MYCROFT_DATADOG_APP_KEY). The Application key is "
|
|
220
|
+
"the one that carries the scopes, and it needs metrics_read and "
|
|
221
|
+
"timeseries_query.",
|
|
222
|
+
)
|
|
223
|
+
return DatadogSource(DatadogEndpoint(api_key=token, app_key=app_key, site=args.datadog))
|
|
224
|
+
return GrafanaSource(PrometheusEndpoint(args.grafana, args.user, token))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _from_metrics(args: argparse.Namespace) -> int:
|
|
228
|
+
"""Read from a metrics backend rather than local transcripts."""
|
|
229
|
+
try:
|
|
230
|
+
source = _source(args)
|
|
231
|
+
|
|
232
|
+
if args.discover:
|
|
233
|
+
where = f"Datadog ({args.datadog})" if args.datadog else args.grafana
|
|
234
|
+
print(render_readiness(source.discover(), where=where))
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
end = args.today
|
|
238
|
+
parsed = source.fetch(end - timedelta(days=args.days), end)
|
|
239
|
+
except SourceError as error:
|
|
240
|
+
print(f"{PROG}: {error.message}", file=sys.stderr)
|
|
241
|
+
return 1
|
|
242
|
+
|
|
243
|
+
return _emit(parsed, args.today, db=args.db, backend="datadog" if args.datadog else "grafana")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _emit(
|
|
247
|
+
parsed: ParsedUsage,
|
|
248
|
+
today: date,
|
|
249
|
+
*,
|
|
250
|
+
db: Path | None = None,
|
|
251
|
+
backend: str | None = None,
|
|
252
|
+
) -> int:
|
|
253
|
+
"""Render, warning first if the price table has aged.
|
|
254
|
+
|
|
255
|
+
Prices are checked by hand against each vendor's page, and this is a personal project, so the
|
|
256
|
+
table will age. An old table still gives the right shape and roughly the right money, so the
|
|
257
|
+
report prints, but with a warning above it that says how old the prices are.
|
|
258
|
+
"""
|
|
259
|
+
analysis = analyse(parsed)
|
|
260
|
+
try:
|
|
261
|
+
verify_prices(today)
|
|
262
|
+
except StalePriceTableError as error:
|
|
263
|
+
print(
|
|
264
|
+
f"{PROG}: warning - the price table may be out of date, so dollar figures may be "
|
|
265
|
+
f"off.\n{error}\n",
|
|
266
|
+
file=sys.stderr,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
since: Comparison | None = None
|
|
270
|
+
if db is not None:
|
|
271
|
+
connection = open_store(db)
|
|
272
|
+
try:
|
|
273
|
+
since = compare(connection, save(connection, parsed, analysis, backend=backend))
|
|
274
|
+
finally:
|
|
275
|
+
connection.close()
|
|
276
|
+
|
|
277
|
+
print(render(parsed, analysis, today=today, since=since))
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Turning parsed usage into findings a person can act on."""
|