costplan 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.
- costplan-0.1.0/LICENSE +21 -0
- costplan-0.1.0/PKG-INFO +229 -0
- costplan-0.1.0/README.md +209 -0
- costplan-0.1.0/pyproject.toml +37 -0
- costplan-0.1.0/setup.cfg +4 -0
- costplan-0.1.0/src/costplan/__init__.py +36 -0
- costplan-0.1.0/src/costplan/_stats.py +109 -0
- costplan-0.1.0/src/costplan/cli.py +165 -0
- costplan-0.1.0/src/costplan/loading.py +264 -0
- costplan-0.1.0/src/costplan/planning.py +416 -0
- costplan-0.1.0/src/costplan.egg-info/PKG-INFO +229 -0
- costplan-0.1.0/src/costplan.egg-info/SOURCES.txt +18 -0
- costplan-0.1.0/src/costplan.egg-info/dependency_links.txt +1 -0
- costplan-0.1.0/src/costplan.egg-info/entry_points.txt +2 -0
- costplan-0.1.0/src/costplan.egg-info/requires.txt +3 -0
- costplan-0.1.0/src/costplan.egg-info/top_level.txt +1 -0
- costplan-0.1.0/tests/test_basics.py +128 -0
- costplan-0.1.0/tests/test_cli.py +36 -0
- costplan-0.1.0/tests/test_loading.py +134 -0
- costplan-0.1.0/tests/test_simulation.py +154 -0
costplan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Meenal Asesh
|
|
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.
|
costplan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: costplan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: How much will my agent cost? Plan AI agent budgets, caps and spend from a handful of pilot runs
|
|
5
|
+
Author: Meenal Asesh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ai-with-md/costplan
|
|
8
|
+
Project-URL: Calculator, https://ai-with-md.github.io/costplan/
|
|
9
|
+
Project-URL: Issues, https://github.com/ai-with-md/costplan/issues
|
|
10
|
+
Keywords: llm,ai agents,cost,budget,token cost,llm cost,cost per task,statistics,forecasting
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# costplan
|
|
22
|
+
|
|
23
|
+
**How much will my agent cost?** Plan AI agent budgets, caps and spend from a handful of pilot runs, with a [calculator that runs in your browser](https://ai-with-md.github.io/costplan/).
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/costplan/)
|
|
26
|
+
[](https://github.com/ai-with-md/costplan/actions/workflows/tests.yml)
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
[](https://ai-with-md.github.io/costplan/)
|
|
31
|
+
|
|
32
|
+
Agent costs are lopsided. Most runs take a few tool calls and cost a few cents. A few get stuck in a loop, retry, or read a long document, and cost ten times more. In the example pilot above, the typical run costs $0.11 and the most expensive one $1.43. That makes averages slow to settle, and it makes "what budget cap should I set?" a question about the rare expensive runs, not the typical ones.
|
|
33
|
+
|
|
34
|
+
costplan answers the planning questions from a pilot:
|
|
35
|
+
|
|
36
|
+
1. What does a run really cost, and what does a **finished task** cost?
|
|
37
|
+
2. What per-run budget cap should I set, so it almost never stops a normal run?
|
|
38
|
+
3. How many pilot runs do I need before I can trust those numbers?
|
|
39
|
+
4. What will it cost at 10,000 runs a day?
|
|
40
|
+
5. Is the cheaper model actually cheaper per finished task?
|
|
41
|
+
|
|
42
|
+
## The number most people get wrong
|
|
43
|
+
|
|
44
|
+
To set a budget cap that stops at most 2% of normal runs, and be 95% sure of it, you need **149 pilot runs**. Fewer runs simply cannot rule out a rare expensive run that a tighter cap would cut off. Set the cap at the most expensive pilot run and this is the best you can promise:
|
|
45
|
+
|
|
46
|
+
| Pilot runs | Cap stops at most |
|
|
47
|
+
|---|---|
|
|
48
|
+
| 20 | 13.9% of normal runs |
|
|
49
|
+
| 30 | 9.5% |
|
|
50
|
+
| 50 | 5.8% |
|
|
51
|
+
| 100 | 3.0% |
|
|
52
|
+
| 149 | 2.0% |
|
|
53
|
+
| 300 | 1.0% |
|
|
54
|
+
|
|
55
|
+
These numbers hold whatever the shape of your costs. costplan tells you what your current pilot can promise, and how many more runs the promise you want would take.
|
|
56
|
+
|
|
57
|
+
## Try it
|
|
58
|
+
|
|
59
|
+
**In the browser:** [ai-with-md.github.io/costplan](https://ai-with-md.github.io/costplan/). Paste your run costs, nothing to install.
|
|
60
|
+
|
|
61
|
+
**In Python:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install costplan
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No dependencies beyond the standard library. Python 3.9 or newer.
|
|
68
|
+
|
|
69
|
+
## Use
|
|
70
|
+
|
|
71
|
+
### Reading your exports
|
|
72
|
+
|
|
73
|
+
`load_runs` reads a CSV, JSON or JSON Lines export from your tracing tool or provider and finds the cost, token, success and run columns by their common names (`cost`, `total_cost`, `totalCost`, `calculatedTotalCost`, `prompt_tokens`, `input_tokens`, `usage.input`, `status`, `error`, `trace_id`, `traceId` and others).
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import costplan as cp
|
|
77
|
+
|
|
78
|
+
runs = cp.load_runs("calls_export.jsonl", group_by="trace_id")
|
|
79
|
+
print(runs.report())
|
|
80
|
+
# Read 318 rows into 40 runs (cost from 'cost', status from 'status', runs from 'trace_id').
|
|
81
|
+
|
|
82
|
+
costs, successes = runs.costs, runs.successes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **One row per model call?** Pass `group_by` with the column that names the run (a trace or session id). Calls are added up into runs, and a run counts as failed if any of its calls failed.
|
|
86
|
+
- **One row per run, already totalled?** Load it as it is. Grouping it again would count costs twice.
|
|
87
|
+
- **No cost column, or calls your tool could not price?** Pass `input_price` and `output_price` in dollars per million tokens and costplan prices them from the token columns. costplan ships no price table, because prices change; take them from your provider's pricing page or a list such as [tokencost](https://github.com/AgentOps-AI/tokencost).
|
|
88
|
+
- **Names not found?** Pass them yourself: `cost="spend"`, `success="passed"`, and dotted names for nested JSON such as `"usage.prompt_tokens"`.
|
|
89
|
+
|
|
90
|
+
The column guesses are tested on files shaped like LangSmith run exports and Langfuse observation exports, using the field names in their documentation. Check `runs.report()` the first time you load a new kind of file.
|
|
91
|
+
|
|
92
|
+
`run_cost` does the same pricing for a single call:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
cp.run_cost(input_tokens=12_000, output_tokens=800, input_price=3.0, output_price=15.0)
|
|
96
|
+
# 0.048
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 1. What does a run cost?
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
print(cp.summarize(costs, successes))
|
|
103
|
+
# 60 runs.
|
|
104
|
+
# Average cost per run: $0.1577 (95% interval $0.1160 to $0.2167)
|
|
105
|
+
# Typical run (median): $0.1100 (95% interval $0.0920 to $0.1371)
|
|
106
|
+
# 1 in 20 runs costs more than: $0.3272 (95% interval needs more runs)
|
|
107
|
+
# Most expensive run: $1.43
|
|
108
|
+
# Success rate: 85.0% (95% interval 73.9% to 91.9%)
|
|
109
|
+
# Cost per successful task: $0.1855 (95% interval $0.1308 to $0.2677)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The cost per successful task is the number that matters for a product: failed runs cost money too.
|
|
113
|
+
|
|
114
|
+
### 2. What cap should I set?
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
print(cp.recommend_cap(costs, stop_rate=0.02))
|
|
118
|
+
# Goal: a cap that stops at most 2.0% of normal runs (95% confidence), from 60 pilot runs.
|
|
119
|
+
# Not enough runs to promise that yet: it takes 149.
|
|
120
|
+
# With 60 runs, a cap at the most expensive run ($1.43) can only promise to stop at most 4.9% of runs.
|
|
121
|
+
# Best single guess for the 98th percentile: $0.7737 (a guess, not a promise).
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With enough runs, it returns a recommended cap. That cap is an upper confidence bound on the cost percentile, so it holds even if the pilot happened to miss some expensive runs. Enforce it with your agent framework, your own code, or a library such as [AgentBudget](https://github.com/AgentBudget/agentbudget).
|
|
125
|
+
|
|
126
|
+
### 3. How many pilot runs do I need?
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
cp.runs_needed_for_cap(stop_rate=0.02) # 149
|
|
130
|
+
cp.guaranteed_stop_rate(n_runs=30) # 0.095
|
|
131
|
+
cp.pilot_runs_for_average(spread=1.0, margin=0.10) # 385 runs to pin the average within 10%
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`spread` is the standard deviation divided by the average. Agent costs often have a spread near 1 or above because of the runaway runs, which is why averages need hundreds of runs to settle.
|
|
135
|
+
|
|
136
|
+
### 4. What will it cost at scale?
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
print(cp.project_spend(costs, runs=300_000))
|
|
140
|
+
# 300,000 runs: about $47,301.75 (95% range $31,765.55 to $62,837.95).
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
At high volume the run to run variation averages out, and nearly all of the range comes from how well the pilot pinned down the average. A bigger pilot narrows it more than anything else.
|
|
144
|
+
|
|
145
|
+
### 5. Is the cheaper model cheaper per finished task?
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
print(cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model")))
|
|
149
|
+
# Cost per successful task: large model $0.1855, small model $0.2197.
|
|
150
|
+
# small model costs 1.18 times as much as large model per success (95% interval 0.71 to 2.02, separate pilots). The difference could be noise.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In this example the small model is about 30% cheaper per run, but it fails and loops more often, so it probably costs more per finished task. With 60 runs each, costplan correctly says the difference is not yet certain.
|
|
154
|
+
|
|
155
|
+
**Run both models on the same tasks if you can.** Hard tasks are expensive for every model, so comparing task by task removes that variation. With `paired=True` the same 60 tasks give an answer that separate pilots cannot:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model"))
|
|
159
|
+
# small model costs 0.73 times as much as large model per success (95% interval 0.49 to 1.08, separate pilots). The difference could be noise.
|
|
160
|
+
|
|
161
|
+
cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model"), paired=True)
|
|
162
|
+
# small model costs 0.73 times as much as large model per success (95% interval 0.61 to 0.88, same tasks, compared task by task). small model is reliably cheaper per success.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
In simulation, pairing gives intervals about half as wide from the same number of runs.
|
|
166
|
+
|
|
167
|
+
## Command line
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
costplan summary runs.csv
|
|
171
|
+
costplan cap runs.csv --stop-rate 0.02
|
|
172
|
+
costplan project runs.csv --runs 300000
|
|
173
|
+
costplan compare large.csv small.csv --names "large model" "small model"
|
|
174
|
+
costplan compare large.csv small.csv --paired --task-column task_id
|
|
175
|
+
costplan summary calls_export.jsonl --group-by trace_id -v
|
|
176
|
+
costplan pilot --stop-rate 0.02 --spread 1.0
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Files can be CSV, JSON or JSON Lines, with the same automatic column finding as `load_runs`. Use `--cost-column`, `--success-column` and similar options when the guess is wrong, `--input-price` and `--output-price` for files with tokens but no costs, and `-v` to see which columns were used. Try it on the simulated files in [`examples/`](https://github.com/ai-with-md/costplan/tree/main/examples).
|
|
180
|
+
|
|
181
|
+
## How the numbers are checked
|
|
182
|
+
|
|
183
|
+
The test suite simulates thousands of pilots with lopsided costs where the truth is known, and checks each promise:
|
|
184
|
+
|
|
185
|
+
| Promise | Held in simulation |
|
|
186
|
+
|---|---|
|
|
187
|
+
| The recommended cap stops at most 2% of runs | 98.5% of pilots (95% promised) |
|
|
188
|
+
| A cap at the most expensive of 149 runs stops at most 2% | 94.8% |
|
|
189
|
+
| The interval for cost per successful task covers the truth | 93% |
|
|
190
|
+
| The spend projection covers the real bill | 92% |
|
|
191
|
+
| The interval for the average cost covers the truth | 92% |
|
|
192
|
+
| The paired comparison interval covers the true ratio | 96% |
|
|
193
|
+
| Paired vs separate pilots, same number of runs | interval about half as wide |
|
|
194
|
+
|
|
195
|
+
The caps and quantiles make no assumption about the shape of costs and keep their promise. The intervals for averages run slightly short of 95% on lopsided costs, which is typical for the bootstrap; treat them as a little optimistic, especially below about 30 runs. See [`tests/test_simulation.py`](https://github.com/ai-with-md/costplan/blob/main/tests/test_simulation.py).
|
|
196
|
+
|
|
197
|
+
## How it compares
|
|
198
|
+
|
|
199
|
+
- **[AgentBudget](https://github.com/AgentBudget/agentbudget)** and similar tools enforce a dollar limit while an agent runs. costplan tells you what limit to set, and what you will spend. They work well together.
|
|
200
|
+
- **Price calculators and price lists** such as [tokencost](https://github.com/AgentOps-AI/tokencost) cost out a single request. costplan works on whole runs, including the expensive tail, retries and failures.
|
|
201
|
+
- **Tracing tools** (Langfuse, LangSmith and others) record what runs cost. costplan turns those records into planning numbers with honest uncertainty.
|
|
202
|
+
|
|
203
|
+
## Limits
|
|
204
|
+
|
|
205
|
+
- The pilot must look like real traffic: the same prompts, tools, models and mix of requests. Change any of them and run a new pilot.
|
|
206
|
+
- Caps and projections assume future runs behave like pilot runs. A new kind of request that triggers long loops will not show up in a pilot that never saw it.
|
|
207
|
+
- The spend projection uses a normal approximation, which is a little narrow for very lopsided costs.
|
|
208
|
+
|
|
209
|
+
## More tools
|
|
210
|
+
|
|
211
|
+
Small, free tools for building and checking AI systems:
|
|
212
|
+
|
|
213
|
+
- **[sturdytools](https://github.com/ai-with-md/sturdytools)**: retries, timeouts, rate limits and circuit breakers for the tools an AI agent calls.
|
|
214
|
+
- **[evalsize](https://github.com/ai-with-md/evalsize)**: how many eval examples you need before a difference between prompts or models means anything.
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m venv .venv
|
|
220
|
+
source .venv/bin/activate
|
|
221
|
+
pip install -e ".[dev]"
|
|
222
|
+
pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The calculator is a [marimo](https://marimo.io) notebook in [`app/calculator.py`](https://github.com/ai-with-md/costplan/blob/main/app/calculator.py), published to GitHub Pages as a WebAssembly app on every push to `main`.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
costplan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# costplan
|
|
2
|
+
|
|
3
|
+
**How much will my agent cost?** Plan AI agent budgets, caps and spend from a handful of pilot runs, with a [calculator that runs in your browser](https://ai-with-md.github.io/costplan/).
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/costplan/)
|
|
6
|
+
[](https://github.com/ai-with-md/costplan/actions/workflows/tests.yml)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
[](https://ai-with-md.github.io/costplan/)
|
|
11
|
+
|
|
12
|
+
Agent costs are lopsided. Most runs take a few tool calls and cost a few cents. A few get stuck in a loop, retry, or read a long document, and cost ten times more. In the example pilot above, the typical run costs $0.11 and the most expensive one $1.43. That makes averages slow to settle, and it makes "what budget cap should I set?" a question about the rare expensive runs, not the typical ones.
|
|
13
|
+
|
|
14
|
+
costplan answers the planning questions from a pilot:
|
|
15
|
+
|
|
16
|
+
1. What does a run really cost, and what does a **finished task** cost?
|
|
17
|
+
2. What per-run budget cap should I set, so it almost never stops a normal run?
|
|
18
|
+
3. How many pilot runs do I need before I can trust those numbers?
|
|
19
|
+
4. What will it cost at 10,000 runs a day?
|
|
20
|
+
5. Is the cheaper model actually cheaper per finished task?
|
|
21
|
+
|
|
22
|
+
## The number most people get wrong
|
|
23
|
+
|
|
24
|
+
To set a budget cap that stops at most 2% of normal runs, and be 95% sure of it, you need **149 pilot runs**. Fewer runs simply cannot rule out a rare expensive run that a tighter cap would cut off. Set the cap at the most expensive pilot run and this is the best you can promise:
|
|
25
|
+
|
|
26
|
+
| Pilot runs | Cap stops at most |
|
|
27
|
+
|---|---|
|
|
28
|
+
| 20 | 13.9% of normal runs |
|
|
29
|
+
| 30 | 9.5% |
|
|
30
|
+
| 50 | 5.8% |
|
|
31
|
+
| 100 | 3.0% |
|
|
32
|
+
| 149 | 2.0% |
|
|
33
|
+
| 300 | 1.0% |
|
|
34
|
+
|
|
35
|
+
These numbers hold whatever the shape of your costs. costplan tells you what your current pilot can promise, and how many more runs the promise you want would take.
|
|
36
|
+
|
|
37
|
+
## Try it
|
|
38
|
+
|
|
39
|
+
**In the browser:** [ai-with-md.github.io/costplan](https://ai-with-md.github.io/costplan/). Paste your run costs, nothing to install.
|
|
40
|
+
|
|
41
|
+
**In Python:**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install costplan
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No dependencies beyond the standard library. Python 3.9 or newer.
|
|
48
|
+
|
|
49
|
+
## Use
|
|
50
|
+
|
|
51
|
+
### Reading your exports
|
|
52
|
+
|
|
53
|
+
`load_runs` reads a CSV, JSON or JSON Lines export from your tracing tool or provider and finds the cost, token, success and run columns by their common names (`cost`, `total_cost`, `totalCost`, `calculatedTotalCost`, `prompt_tokens`, `input_tokens`, `usage.input`, `status`, `error`, `trace_id`, `traceId` and others).
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import costplan as cp
|
|
57
|
+
|
|
58
|
+
runs = cp.load_runs("calls_export.jsonl", group_by="trace_id")
|
|
59
|
+
print(runs.report())
|
|
60
|
+
# Read 318 rows into 40 runs (cost from 'cost', status from 'status', runs from 'trace_id').
|
|
61
|
+
|
|
62
|
+
costs, successes = runs.costs, runs.successes
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **One row per model call?** Pass `group_by` with the column that names the run (a trace or session id). Calls are added up into runs, and a run counts as failed if any of its calls failed.
|
|
66
|
+
- **One row per run, already totalled?** Load it as it is. Grouping it again would count costs twice.
|
|
67
|
+
- **No cost column, or calls your tool could not price?** Pass `input_price` and `output_price` in dollars per million tokens and costplan prices them from the token columns. costplan ships no price table, because prices change; take them from your provider's pricing page or a list such as [tokencost](https://github.com/AgentOps-AI/tokencost).
|
|
68
|
+
- **Names not found?** Pass them yourself: `cost="spend"`, `success="passed"`, and dotted names for nested JSON such as `"usage.prompt_tokens"`.
|
|
69
|
+
|
|
70
|
+
The column guesses are tested on files shaped like LangSmith run exports and Langfuse observation exports, using the field names in their documentation. Check `runs.report()` the first time you load a new kind of file.
|
|
71
|
+
|
|
72
|
+
`run_cost` does the same pricing for a single call:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
cp.run_cost(input_tokens=12_000, output_tokens=800, input_price=3.0, output_price=15.0)
|
|
76
|
+
# 0.048
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 1. What does a run cost?
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
print(cp.summarize(costs, successes))
|
|
83
|
+
# 60 runs.
|
|
84
|
+
# Average cost per run: $0.1577 (95% interval $0.1160 to $0.2167)
|
|
85
|
+
# Typical run (median): $0.1100 (95% interval $0.0920 to $0.1371)
|
|
86
|
+
# 1 in 20 runs costs more than: $0.3272 (95% interval needs more runs)
|
|
87
|
+
# Most expensive run: $1.43
|
|
88
|
+
# Success rate: 85.0% (95% interval 73.9% to 91.9%)
|
|
89
|
+
# Cost per successful task: $0.1855 (95% interval $0.1308 to $0.2677)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The cost per successful task is the number that matters for a product: failed runs cost money too.
|
|
93
|
+
|
|
94
|
+
### 2. What cap should I set?
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
print(cp.recommend_cap(costs, stop_rate=0.02))
|
|
98
|
+
# Goal: a cap that stops at most 2.0% of normal runs (95% confidence), from 60 pilot runs.
|
|
99
|
+
# Not enough runs to promise that yet: it takes 149.
|
|
100
|
+
# With 60 runs, a cap at the most expensive run ($1.43) can only promise to stop at most 4.9% of runs.
|
|
101
|
+
# Best single guess for the 98th percentile: $0.7737 (a guess, not a promise).
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
With enough runs, it returns a recommended cap. That cap is an upper confidence bound on the cost percentile, so it holds even if the pilot happened to miss some expensive runs. Enforce it with your agent framework, your own code, or a library such as [AgentBudget](https://github.com/AgentBudget/agentbudget).
|
|
105
|
+
|
|
106
|
+
### 3. How many pilot runs do I need?
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
cp.runs_needed_for_cap(stop_rate=0.02) # 149
|
|
110
|
+
cp.guaranteed_stop_rate(n_runs=30) # 0.095
|
|
111
|
+
cp.pilot_runs_for_average(spread=1.0, margin=0.10) # 385 runs to pin the average within 10%
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`spread` is the standard deviation divided by the average. Agent costs often have a spread near 1 or above because of the runaway runs, which is why averages need hundreds of runs to settle.
|
|
115
|
+
|
|
116
|
+
### 4. What will it cost at scale?
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
print(cp.project_spend(costs, runs=300_000))
|
|
120
|
+
# 300,000 runs: about $47,301.75 (95% range $31,765.55 to $62,837.95).
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
At high volume the run to run variation averages out, and nearly all of the range comes from how well the pilot pinned down the average. A bigger pilot narrows it more than anything else.
|
|
124
|
+
|
|
125
|
+
### 5. Is the cheaper model cheaper per finished task?
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
print(cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model")))
|
|
129
|
+
# Cost per successful task: large model $0.1855, small model $0.2197.
|
|
130
|
+
# small model costs 1.18 times as much as large model per success (95% interval 0.71 to 2.02, separate pilots). The difference could be noise.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
In this example the small model is about 30% cheaper per run, but it fails and loops more often, so it probably costs more per finished task. With 60 runs each, costplan correctly says the difference is not yet certain.
|
|
134
|
+
|
|
135
|
+
**Run both models on the same tasks if you can.** Hard tasks are expensive for every model, so comparing task by task removes that variation. With `paired=True` the same 60 tasks give an answer that separate pilots cannot:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model"))
|
|
139
|
+
# small model costs 0.73 times as much as large model per success (95% interval 0.49 to 1.08, separate pilots). The difference could be noise.
|
|
140
|
+
|
|
141
|
+
cp.compare_cost_per_success(large_costs, large_ok, small_costs, small_ok, names=("large model", "small model"), paired=True)
|
|
142
|
+
# small model costs 0.73 times as much as large model per success (95% interval 0.61 to 0.88, same tasks, compared task by task). small model is reliably cheaper per success.
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
In simulation, pairing gives intervals about half as wide from the same number of runs.
|
|
146
|
+
|
|
147
|
+
## Command line
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
costplan summary runs.csv
|
|
151
|
+
costplan cap runs.csv --stop-rate 0.02
|
|
152
|
+
costplan project runs.csv --runs 300000
|
|
153
|
+
costplan compare large.csv small.csv --names "large model" "small model"
|
|
154
|
+
costplan compare large.csv small.csv --paired --task-column task_id
|
|
155
|
+
costplan summary calls_export.jsonl --group-by trace_id -v
|
|
156
|
+
costplan pilot --stop-rate 0.02 --spread 1.0
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Files can be CSV, JSON or JSON Lines, with the same automatic column finding as `load_runs`. Use `--cost-column`, `--success-column` and similar options when the guess is wrong, `--input-price` and `--output-price` for files with tokens but no costs, and `-v` to see which columns were used. Try it on the simulated files in [`examples/`](https://github.com/ai-with-md/costplan/tree/main/examples).
|
|
160
|
+
|
|
161
|
+
## How the numbers are checked
|
|
162
|
+
|
|
163
|
+
The test suite simulates thousands of pilots with lopsided costs where the truth is known, and checks each promise:
|
|
164
|
+
|
|
165
|
+
| Promise | Held in simulation |
|
|
166
|
+
|---|---|
|
|
167
|
+
| The recommended cap stops at most 2% of runs | 98.5% of pilots (95% promised) |
|
|
168
|
+
| A cap at the most expensive of 149 runs stops at most 2% | 94.8% |
|
|
169
|
+
| The interval for cost per successful task covers the truth | 93% |
|
|
170
|
+
| The spend projection covers the real bill | 92% |
|
|
171
|
+
| The interval for the average cost covers the truth | 92% |
|
|
172
|
+
| The paired comparison interval covers the true ratio | 96% |
|
|
173
|
+
| Paired vs separate pilots, same number of runs | interval about half as wide |
|
|
174
|
+
|
|
175
|
+
The caps and quantiles make no assumption about the shape of costs and keep their promise. The intervals for averages run slightly short of 95% on lopsided costs, which is typical for the bootstrap; treat them as a little optimistic, especially below about 30 runs. See [`tests/test_simulation.py`](https://github.com/ai-with-md/costplan/blob/main/tests/test_simulation.py).
|
|
176
|
+
|
|
177
|
+
## How it compares
|
|
178
|
+
|
|
179
|
+
- **[AgentBudget](https://github.com/AgentBudget/agentbudget)** and similar tools enforce a dollar limit while an agent runs. costplan tells you what limit to set, and what you will spend. They work well together.
|
|
180
|
+
- **Price calculators and price lists** such as [tokencost](https://github.com/AgentOps-AI/tokencost) cost out a single request. costplan works on whole runs, including the expensive tail, retries and failures.
|
|
181
|
+
- **Tracing tools** (Langfuse, LangSmith and others) record what runs cost. costplan turns those records into planning numbers with honest uncertainty.
|
|
182
|
+
|
|
183
|
+
## Limits
|
|
184
|
+
|
|
185
|
+
- The pilot must look like real traffic: the same prompts, tools, models and mix of requests. Change any of them and run a new pilot.
|
|
186
|
+
- Caps and projections assume future runs behave like pilot runs. A new kind of request that triggers long loops will not show up in a pilot that never saw it.
|
|
187
|
+
- The spend projection uses a normal approximation, which is a little narrow for very lopsided costs.
|
|
188
|
+
|
|
189
|
+
## More tools
|
|
190
|
+
|
|
191
|
+
Small, free tools for building and checking AI systems:
|
|
192
|
+
|
|
193
|
+
- **[sturdytools](https://github.com/ai-with-md/sturdytools)**: retries, timeouts, rate limits and circuit breakers for the tools an AI agent calls.
|
|
194
|
+
- **[evalsize](https://github.com/ai-with-md/evalsize)**: how many eval examples you need before a difference between prompts or models means anything.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python -m venv .venv
|
|
200
|
+
source .venv/bin/activate
|
|
201
|
+
pip install -e ".[dev]"
|
|
202
|
+
pytest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The calculator is a [marimo](https://marimo.io) notebook in [`app/calculator.py`](https://github.com/ai-with-md/costplan/blob/main/app/calculator.py), published to GitHub Pages as a WebAssembly app on every push to `main`.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "costplan"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "How much will my agent cost? Plan AI agent budgets, caps and spend from a handful of pilot runs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{name = "Meenal Asesh"}]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["llm", "ai agents", "cost", "budget", "token cost", "llm cost", "cost per task", "statistics", "forecasting"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
costplan = "costplan.cli:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/ai-with-md/costplan"
|
|
29
|
+
Calculator = "https://ai-with-md.github.io/costplan/"
|
|
30
|
+
Issues = "https://github.com/ai-with-md/costplan/issues"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests", "src"]
|
|
37
|
+
addopts = "--doctest-modules"
|
costplan-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""How much will my agent cost? Plan agent budgets from a handful of pilot runs."""
|
|
2
|
+
|
|
3
|
+
from .loading import LoadedRuns, load_runs
|
|
4
|
+
from .planning import (
|
|
5
|
+
CapAdvice,
|
|
6
|
+
CostComparison,
|
|
7
|
+
CostSummary,
|
|
8
|
+
SpendProjection,
|
|
9
|
+
compare_cost_per_success,
|
|
10
|
+
guaranteed_stop_rate,
|
|
11
|
+
pilot_runs_for_average,
|
|
12
|
+
project_spend,
|
|
13
|
+
recommend_cap,
|
|
14
|
+
run_cost,
|
|
15
|
+
runs_needed_for_cap,
|
|
16
|
+
summarize,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"load_runs",
|
|
21
|
+
"LoadedRuns",
|
|
22
|
+
"run_cost",
|
|
23
|
+
"summarize",
|
|
24
|
+
"recommend_cap",
|
|
25
|
+
"runs_needed_for_cap",
|
|
26
|
+
"guaranteed_stop_rate",
|
|
27
|
+
"pilot_runs_for_average",
|
|
28
|
+
"project_spend",
|
|
29
|
+
"compare_cost_per_success",
|
|
30
|
+
"CostSummary",
|
|
31
|
+
"CapAdvice",
|
|
32
|
+
"SpendProjection",
|
|
33
|
+
"CostComparison",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Small statistics helpers built on the standard library only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import random
|
|
7
|
+
from statistics import NormalDist
|
|
8
|
+
from typing import Callable, List, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
_NORMAL = NormalDist()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_level(value: float, name: str) -> None:
|
|
14
|
+
if not 0 < value < 1:
|
|
15
|
+
raise ValueError(f"{name} must be strictly between 0 and 1, got {value!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def z_two_sided(confidence: float) -> float:
|
|
19
|
+
return _NORMAL.inv_cdf(1 - (1 - confidence) / 2)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def wilson(successes: int, n: int, confidence: float) -> Tuple[float, float]:
|
|
23
|
+
z = z_two_sided(confidence)
|
|
24
|
+
p = successes / n
|
|
25
|
+
denom = 1 + z * z / n
|
|
26
|
+
centre = (p + z * z / (2 * n)) / denom
|
|
27
|
+
half = z / denom * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n))
|
|
28
|
+
return max(0.0, centre - half), min(1.0, centre + half)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def binom_cdf_table(n: int, p: float) -> List[float]:
|
|
32
|
+
"""[P(X <= k) for k = 0..n] for X ~ Binomial(n, p), computed in log space."""
|
|
33
|
+
if p <= 0:
|
|
34
|
+
return [1.0] * (n + 1)
|
|
35
|
+
if p >= 1:
|
|
36
|
+
return [0.0] * n + [1.0]
|
|
37
|
+
log_p, log_q = math.log(p), math.log1p(-p)
|
|
38
|
+
log_n = math.lgamma(n + 1)
|
|
39
|
+
table, total = [], 0.0
|
|
40
|
+
for i in range(n + 1):
|
|
41
|
+
total += math.exp(log_n - math.lgamma(i + 1) - math.lgamma(n - i + 1) + i * log_p + (n - i) * log_q)
|
|
42
|
+
table.append(min(1.0, total))
|
|
43
|
+
return table
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def quantile(sorted_values: Sequence[float], p: float) -> float:
|
|
47
|
+
"""Linear interpolation quantile (the usual default in numpy and pandas)."""
|
|
48
|
+
n = len(sorted_values)
|
|
49
|
+
if n == 1:
|
|
50
|
+
return sorted_values[0]
|
|
51
|
+
pos = p * (n - 1)
|
|
52
|
+
lo = math.floor(pos)
|
|
53
|
+
hi = min(lo + 1, n - 1)
|
|
54
|
+
return sorted_values[lo] + (sorted_values[hi] - sorted_values[lo]) * (pos - lo)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def quantile_interval(
|
|
58
|
+
sorted_values: Sequence[float], p: float, confidence: float
|
|
59
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
60
|
+
"""Distribution-free interval for the p-th quantile from order statistics.
|
|
61
|
+
|
|
62
|
+
Makes no assumption about the shape of the cost distribution. Either end
|
|
63
|
+
is None when there are too few runs to bound it at this confidence.
|
|
64
|
+
"""
|
|
65
|
+
n = len(sorted_values)
|
|
66
|
+
cdf = binom_cdf_table(n, p)
|
|
67
|
+
tail = (1 - confidence) / 2
|
|
68
|
+
low = None
|
|
69
|
+
for k in range(n, 0, -1): # largest k with P(X_(k) > q) at most the tail
|
|
70
|
+
if cdf[k - 1] <= tail:
|
|
71
|
+
low = sorted_values[k - 1]
|
|
72
|
+
break
|
|
73
|
+
high = upper_quantile_bound(sorted_values, p, 1 - tail, cdf)
|
|
74
|
+
return low, high
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def upper_quantile_bound(
|
|
78
|
+
sorted_values: Sequence[float], p: float, confidence: float, cdf: Optional[List[float]] = None
|
|
79
|
+
) -> Optional[float]:
|
|
80
|
+
"""Smallest order statistic that is above the p-th quantile with the given confidence."""
|
|
81
|
+
n = len(sorted_values)
|
|
82
|
+
cdf = cdf or binom_cdf_table(n, p)
|
|
83
|
+
for k in range(1, n + 1):
|
|
84
|
+
if cdf[k - 1] >= confidence:
|
|
85
|
+
return sorted_values[k - 1]
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def bootstrap_interval(
|
|
90
|
+
columns: Sequence[Sequence],
|
|
91
|
+
statistic: Callable[..., Optional[float]],
|
|
92
|
+
confidence: float,
|
|
93
|
+
resamples: int,
|
|
94
|
+
seed: int,
|
|
95
|
+
) -> Tuple[float, float]:
|
|
96
|
+
"""Percentile bootstrap, resampling rows of the given columns together."""
|
|
97
|
+
rng = random.Random(seed)
|
|
98
|
+
n = len(columns[0])
|
|
99
|
+
values: List[float] = []
|
|
100
|
+
for _ in range(resamples):
|
|
101
|
+
idx = [rng.randrange(n) for _ in range(n)]
|
|
102
|
+
value = statistic(*[[col[i] for i in idx] for col in columns])
|
|
103
|
+
if value is not None and math.isfinite(value):
|
|
104
|
+
values.append(value)
|
|
105
|
+
if not values:
|
|
106
|
+
return float("nan"), float("nan")
|
|
107
|
+
values.sort()
|
|
108
|
+
tail = (1 - confidence) / 2
|
|
109
|
+
return quantile(values, tail), quantile(values, 1 - tail)
|