shekel 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.
- shekel-0.1.0/.github/workflows/ci.yml +47 -0
- shekel-0.1.0/.github/workflows/publish.yml +30 -0
- shekel-0.1.0/.gitignore +17 -0
- shekel-0.1.0/CHANGELOG.md +26 -0
- shekel-0.1.0/CONTRIBUTING.md +46 -0
- shekel-0.1.0/LICENSE +21 -0
- shekel-0.1.0/PKG-INFO +175 -0
- shekel-0.1.0/README.md +116 -0
- shekel-0.1.0/examples/langgraph_demo.py +59 -0
- shekel-0.1.0/pyproject.toml +81 -0
- shekel-0.1.0/shekel/__init__.py +5 -0
- shekel-0.1.0/shekel/_budget.py +137 -0
- shekel-0.1.0/shekel/_context.py +20 -0
- shekel-0.1.0/shekel/_patch.py +285 -0
- shekel-0.1.0/shekel/_pricing.py +59 -0
- shekel-0.1.0/shekel/exceptions.py +35 -0
- shekel-0.1.0/shekel/prices.json +49 -0
- shekel-0.1.0/tests/conftest.py +79 -0
- shekel-0.1.0/tests/test_budget_async.py +97 -0
- shekel-0.1.0/tests/test_budget_sync.py +227 -0
- shekel-0.1.0/tests/test_concurrent.py +86 -0
- shekel-0.1.0/tests/test_coverage_gaps.py +216 -0
- shekel-0.1.0/tests/test_pricing.py +77 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e ".[all,dev]"
|
|
26
|
+
|
|
27
|
+
- name: Format check (black)
|
|
28
|
+
run: black --check .
|
|
29
|
+
|
|
30
|
+
- name: Import sort check (isort)
|
|
31
|
+
run: isort --check-only .
|
|
32
|
+
|
|
33
|
+
- name: Lint (ruff)
|
|
34
|
+
run: ruff check .
|
|
35
|
+
|
|
36
|
+
- name: Type check (mypy)
|
|
37
|
+
run: mypy shekel/
|
|
38
|
+
|
|
39
|
+
- name: Test (pytest)
|
|
40
|
+
run: pytest tests/ -v --cov=shekel --cov-report=xml
|
|
41
|
+
|
|
42
|
+
- name: Upload coverage
|
|
43
|
+
uses: codecov/codecov-action@v4
|
|
44
|
+
if: matrix.python-version == '3.11'
|
|
45
|
+
with:
|
|
46
|
+
files: coverage.xml
|
|
47
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for trusted publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
|
|
23
|
+
- name: Install build
|
|
24
|
+
run: pip install build
|
|
25
|
+
|
|
26
|
+
- name: Build
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
shekel-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2024-03-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `budget()` context manager with sync and async support
|
|
14
|
+
- `BudgetExceededError` with spend/limit/model/token details
|
|
15
|
+
- Monkey-patching for OpenAI `ChatCompletions.create` (sync + async + streaming)
|
|
16
|
+
- Monkey-patching for Anthropic `Messages.create` (sync + async + streaming)
|
|
17
|
+
- `ContextVar`-based isolation for thread-safe and async-safe budget tracking
|
|
18
|
+
- Ref-counted patching (only patches once for nested contexts)
|
|
19
|
+
- `warn_at` threshold with optional `on_exceed` callback
|
|
20
|
+
- `price_per_1k_tokens` override for custom/unlisted models
|
|
21
|
+
- Built-in pricing for 10 models (GPT-4o, GPT-4o-mini, o1, o1-mini, GPT-3.5-turbo, Claude 3.5 Sonnet, Claude 3 Haiku, Claude 3 Opus, Gemini 1.5 Flash, Gemini 1.5 Pro)
|
|
22
|
+
- Track-only mode (`budget()` with no `max_usd`)
|
|
23
|
+
- LangGraph integration example
|
|
24
|
+
|
|
25
|
+
[Unreleased]: https://github.com/arieradle/shekel/compare/v0.1.0...HEAD
|
|
26
|
+
[0.1.0]: https://github.com/arieradle/shekel/releases/tag/v0.1.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Contributing to shekel
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/arieradle/shekel
|
|
9
|
+
cd shekel
|
|
10
|
+
pip install -e ".[all,dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Running checks
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
black . # format
|
|
17
|
+
isort . # sort imports
|
|
18
|
+
ruff check . # lint
|
|
19
|
+
mypy shekel/ # type check
|
|
20
|
+
pytest tests/ -v # run tests
|
|
21
|
+
pytest tests/ --cov=shekel --cov-report=term-missing # with coverage
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Adding a model
|
|
25
|
+
|
|
26
|
+
Edit `shekel/prices.json` and add an entry:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
"model-name": {"input_per_1k": 0.001, "output_per_1k": 0.003}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add a test in `tests/test_pricing.py` to verify the new model's cost calculation.
|
|
33
|
+
|
|
34
|
+
## Pull requests
|
|
35
|
+
|
|
36
|
+
- Keep PRs focused — one change per PR
|
|
37
|
+
- Add tests for new behaviour
|
|
38
|
+
- All CI checks must pass
|
|
39
|
+
- Update `CHANGELOG.md` under `[Unreleased]`
|
|
40
|
+
|
|
41
|
+
## Reporting bugs
|
|
42
|
+
|
|
43
|
+
Open an issue at https://github.com/arieradle/shekel/issues with:
|
|
44
|
+
- Python version
|
|
45
|
+
- shekel version (`python -c "import shekel; print(shekel.__version__)"`)
|
|
46
|
+
- Minimal reproduction
|
shekel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Arie Radle
|
|
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.
|
shekel-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shekel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM cost tracking and budget enforcement. Zero config — with budget(max_usd=1.00): run_agent(). Works with LangGraph, CrewAI, raw OpenAI/Anthropic.
|
|
5
|
+
Project-URL: Homepage, https://github.com/arieradle/shekel
|
|
6
|
+
Project-URL: Repository, https://github.com/arieradle/shekel
|
|
7
|
+
Project-URL: Issues, https://github.com/arieradle/shekel/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/arieradle/shekel/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Arie Radle <arie.radle@gmail.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2024 Arie Radle
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: agent budget,anthropic spend enforcement,crewai cost control,langchain cost enforcement,langgraph budget,llm budget,llm cost tracking,llm spend limit,openai budget limit
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
42
|
+
Requires-Python: >=3.9
|
|
43
|
+
Provides-Extra: all
|
|
44
|
+
Requires-Dist: anthropic>=0.7.0; extra == 'all'
|
|
45
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
46
|
+
Provides-Extra: anthropic
|
|
47
|
+
Requires-Dist: anthropic>=0.7.0; extra == 'anthropic'
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
56
|
+
Provides-Extra: openai
|
|
57
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
58
|
+
Description-Content-Type: text/markdown
|
|
59
|
+
|
|
60
|
+
[](https://badge.fury.io/py/shekel)
|
|
61
|
+
[](https://pypi.org/project/shekel/)
|
|
62
|
+
[](https://github.com/arieradle/shekel/actions/workflows/ci.yml)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
with budget(max_usd=1.00):
|
|
66
|
+
run_my_agent() # raises BudgetExceededError if spend exceeds $1.00
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
I spent $47 debugging a LangGraph retry loop. The agent kept failing, LangGraph kept retrying, and OpenAI kept charging — all while I slept. I built shekel so you don't have to learn that lesson yourself.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install shekel[openai] # OpenAI only
|
|
77
|
+
pip install shekel[anthropic] # Anthropic only
|
|
78
|
+
pip install shekel[all] # Both SDKs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
*Advanced: `pip install shekel` installs the core with no SDK deps (track-only mode).*
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Quick start
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from shekel import budget, BudgetExceededError
|
|
89
|
+
import openai
|
|
90
|
+
|
|
91
|
+
client = openai.OpenAI()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with budget(max_usd=1.00, warn_at=0.8) as b:
|
|
95
|
+
response = client.chat.completions.create(
|
|
96
|
+
model="gpt-4o",
|
|
97
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
98
|
+
)
|
|
99
|
+
print(f"Spent: ${b.spent:.4f} of ${b.limit:.2f}")
|
|
100
|
+
except BudgetExceededError as e:
|
|
101
|
+
print(e)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Track-only mode
|
|
107
|
+
|
|
108
|
+
No `max_usd` = track spend without enforcing a limit. Great for profiling agents.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with budget() as b:
|
|
112
|
+
run_my_agent()
|
|
113
|
+
|
|
114
|
+
print(f"That run cost: ${b.spent:.4f}")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
- **Monkey-patching**: When you enter a `budget()` context, shekel wraps `openai.ChatCompletions.create` and `anthropic.Messages.create` at the class level. Your code calls the real SDK — shekel intercepts the response, reads the token counts, and calculates cost. On context exit, original methods are restored.
|
|
122
|
+
- **ContextVar isolation**: Each `budget()` context tracks its own spend using Python's `contextvars.ContextVar`. Two concurrent agent runs never share a budget counter, even in async or multi-threaded code.
|
|
123
|
+
- **Zero config**: No API keys, no external services, no config files. `pip install` + `with budget(...)` is all you need.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Model support
|
|
128
|
+
|
|
129
|
+
| Model | Input (per 1k) | Output (per 1k) |
|
|
130
|
+
|-------|---------------|-----------------|
|
|
131
|
+
| gpt-4o | $0.00250 | $0.01000 |
|
|
132
|
+
| gpt-4o-mini | $0.000150 | $0.000600 |
|
|
133
|
+
| o1 | $0.01500 | $0.06000 |
|
|
134
|
+
| o1-mini | $0.00300 | $0.01200 |
|
|
135
|
+
| gpt-3.5-turbo | $0.000500 | $0.001500 |
|
|
136
|
+
| claude-3-5-sonnet-20241022 | $0.00300 | $0.01500 |
|
|
137
|
+
| claude-3-haiku-20240307 | $0.000250 | $0.001250 |
|
|
138
|
+
| claude-3-opus-20240229 | $0.01500 | $0.07500 |
|
|
139
|
+
| gemini-1.5-flash | $0.0000750 | $0.000300 |
|
|
140
|
+
| gemini-1.5-pro | $0.00125 | $0.00500 |
|
|
141
|
+
|
|
142
|
+
**Model not listed?** Pass a price override:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
with budget(max_usd=1.00, price_per_1k_tokens={"input": 0.001, "output": 0.003}):
|
|
146
|
+
run_my_agent()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Works with LangGraph, CrewAI, and any framework
|
|
152
|
+
|
|
153
|
+
shekel is framework-agnostic. It intercepts at the SDK level, so it works with anything that calls OpenAI or Anthropic under the hood:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# LangGraph
|
|
157
|
+
with budget(max_usd=2.00, warn_at=0.8) as b:
|
|
158
|
+
result = langgraph_app.invoke({"input": "..."})
|
|
159
|
+
print(f"Graph run cost: ${b.spent:.4f}")
|
|
160
|
+
|
|
161
|
+
# CrewAI
|
|
162
|
+
with budget(max_usd=5.00) as b:
|
|
163
|
+
crew.kickoff()
|
|
164
|
+
|
|
165
|
+
# Raw SDK
|
|
166
|
+
with budget(max_usd=0.50) as b:
|
|
167
|
+
for _ in range(100):
|
|
168
|
+
client.chat.completions.create(...) # stops when budget hit
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
shekel-0.1.0/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[](https://badge.fury.io/py/shekel)
|
|
2
|
+
[](https://pypi.org/project/shekel/)
|
|
3
|
+
[](https://github.com/arieradle/shekel/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
with budget(max_usd=1.00):
|
|
7
|
+
run_my_agent() # raises BudgetExceededError if spend exceeds $1.00
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
I spent $47 debugging a LangGraph retry loop. The agent kept failing, LangGraph kept retrying, and OpenAI kept charging — all while I slept. I built shekel so you don't have to learn that lesson yourself.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install shekel[openai] # OpenAI only
|
|
18
|
+
pip install shekel[anthropic] # Anthropic only
|
|
19
|
+
pip install shekel[all] # Both SDKs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
*Advanced: `pip install shekel` installs the core with no SDK deps (track-only mode).*
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from shekel import budget, BudgetExceededError
|
|
30
|
+
import openai
|
|
31
|
+
|
|
32
|
+
client = openai.OpenAI()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with budget(max_usd=1.00, warn_at=0.8) as b:
|
|
36
|
+
response = client.chat.completions.create(
|
|
37
|
+
model="gpt-4o",
|
|
38
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
39
|
+
)
|
|
40
|
+
print(f"Spent: ${b.spent:.4f} of ${b.limit:.2f}")
|
|
41
|
+
except BudgetExceededError as e:
|
|
42
|
+
print(e)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Track-only mode
|
|
48
|
+
|
|
49
|
+
No `max_usd` = track spend without enforcing a limit. Great for profiling agents.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
with budget() as b:
|
|
53
|
+
run_my_agent()
|
|
54
|
+
|
|
55
|
+
print(f"That run cost: ${b.spent:.4f}")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
- **Monkey-patching**: When you enter a `budget()` context, shekel wraps `openai.ChatCompletions.create` and `anthropic.Messages.create` at the class level. Your code calls the real SDK — shekel intercepts the response, reads the token counts, and calculates cost. On context exit, original methods are restored.
|
|
63
|
+
- **ContextVar isolation**: Each `budget()` context tracks its own spend using Python's `contextvars.ContextVar`. Two concurrent agent runs never share a budget counter, even in async or multi-threaded code.
|
|
64
|
+
- **Zero config**: No API keys, no external services, no config files. `pip install` + `with budget(...)` is all you need.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Model support
|
|
69
|
+
|
|
70
|
+
| Model | Input (per 1k) | Output (per 1k) |
|
|
71
|
+
|-------|---------------|-----------------|
|
|
72
|
+
| gpt-4o | $0.00250 | $0.01000 |
|
|
73
|
+
| gpt-4o-mini | $0.000150 | $0.000600 |
|
|
74
|
+
| o1 | $0.01500 | $0.06000 |
|
|
75
|
+
| o1-mini | $0.00300 | $0.01200 |
|
|
76
|
+
| gpt-3.5-turbo | $0.000500 | $0.001500 |
|
|
77
|
+
| claude-3-5-sonnet-20241022 | $0.00300 | $0.01500 |
|
|
78
|
+
| claude-3-haiku-20240307 | $0.000250 | $0.001250 |
|
|
79
|
+
| claude-3-opus-20240229 | $0.01500 | $0.07500 |
|
|
80
|
+
| gemini-1.5-flash | $0.0000750 | $0.000300 |
|
|
81
|
+
| gemini-1.5-pro | $0.00125 | $0.00500 |
|
|
82
|
+
|
|
83
|
+
**Model not listed?** Pass a price override:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
with budget(max_usd=1.00, price_per_1k_tokens={"input": 0.001, "output": 0.003}):
|
|
87
|
+
run_my_agent()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Works with LangGraph, CrewAI, and any framework
|
|
93
|
+
|
|
94
|
+
shekel is framework-agnostic. It intercepts at the SDK level, so it works with anything that calls OpenAI or Anthropic under the hood:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# LangGraph
|
|
98
|
+
with budget(max_usd=2.00, warn_at=0.8) as b:
|
|
99
|
+
result = langgraph_app.invoke({"input": "..."})
|
|
100
|
+
print(f"Graph run cost: ${b.spent:.4f}")
|
|
101
|
+
|
|
102
|
+
# CrewAI
|
|
103
|
+
with budget(max_usd=5.00) as b:
|
|
104
|
+
crew.kickoff()
|
|
105
|
+
|
|
106
|
+
# Raw SDK
|
|
107
|
+
with budget(max_usd=0.50) as b:
|
|
108
|
+
for _ in range(100):
|
|
109
|
+
client.chat.completions.create(...) # stops when budget hit
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Requires: pip install shekel[openai] "langgraph>=0.2"
|
|
2
|
+
"""
|
|
3
|
+
Minimal LangGraph demo showing shekel budget enforcement.
|
|
4
|
+
|
|
5
|
+
This example builds a simple one-node LangGraph that calls OpenAI,
|
|
6
|
+
wrapped in a shekel budget context to track and cap spend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from shekel import BudgetExceededError, budget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
try:
|
|
16
|
+
import openai
|
|
17
|
+
from langgraph.graph import END, StateGraph # type: ignore[import]
|
|
18
|
+
from typing_extensions import TypedDict
|
|
19
|
+
except ImportError as e:
|
|
20
|
+
print(f"Missing dependency: {e}")
|
|
21
|
+
print("Run: pip install shekel[openai] 'langgraph>=0.2' typing_extensions")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
25
|
+
if not api_key:
|
|
26
|
+
print("Set OPENAI_API_KEY to run this demo.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
client = openai.OpenAI(api_key=api_key)
|
|
30
|
+
|
|
31
|
+
class State(TypedDict):
|
|
32
|
+
question: str
|
|
33
|
+
answer: str
|
|
34
|
+
|
|
35
|
+
def call_llm(state: State) -> State:
|
|
36
|
+
response = client.chat.completions.create(
|
|
37
|
+
model="gpt-4o-mini",
|
|
38
|
+
messages=[{"role": "user", "content": state["question"]}],
|
|
39
|
+
max_tokens=50,
|
|
40
|
+
)
|
|
41
|
+
return {"question": state["question"], "answer": response.choices[0].message.content}
|
|
42
|
+
|
|
43
|
+
graph = StateGraph(State)
|
|
44
|
+
graph.add_node("llm", call_llm)
|
|
45
|
+
graph.set_entry_point("llm")
|
|
46
|
+
graph.add_edge("llm", END)
|
|
47
|
+
app = graph.compile()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with budget(max_usd=0.10, warn_at=0.8) as b:
|
|
51
|
+
result = app.invoke({"question": "What is 2+2?", "answer": ""})
|
|
52
|
+
print(f"Answer: {result['answer']}")
|
|
53
|
+
print(f"Spent: ${b.spent:.4f} / ${b.limit:.2f}")
|
|
54
|
+
except BudgetExceededError as e:
|
|
55
|
+
print(f"Budget exceeded: {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shekel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LLM cost tracking and budget enforcement. Zero config — with budget(max_usd=1.00): run_agent(). Works with LangGraph, CrewAI, raw OpenAI/Anthropic."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Arie Radle", email = "arie.radle@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = [
|
|
14
|
+
"llm cost tracking",
|
|
15
|
+
"openai budget limit",
|
|
16
|
+
"anthropic spend enforcement",
|
|
17
|
+
"langchain cost enforcement",
|
|
18
|
+
"llm spend limit",
|
|
19
|
+
"agent budget",
|
|
20
|
+
"llm budget",
|
|
21
|
+
"langgraph budget",
|
|
22
|
+
"crewai cost control",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.9",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
]
|
|
35
|
+
# Zero hard dependencies — stdlib only
|
|
36
|
+
dependencies = []
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
openai = ["openai>=1.0.0"]
|
|
40
|
+
anthropic = ["anthropic>=0.7.0"]
|
|
41
|
+
all = ["openai>=1.0.0", "anthropic>=0.7.0"]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=7.0.0",
|
|
44
|
+
"pytest-asyncio>=0.21.0",
|
|
45
|
+
"ruff>=0.1.0",
|
|
46
|
+
"mypy>=1.0.0",
|
|
47
|
+
"black>=23.0.0",
|
|
48
|
+
"isort>=5.12.0",
|
|
49
|
+
"pytest-cov>=4.0.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/arieradle/shekel"
|
|
54
|
+
Repository = "https://github.com/arieradle/shekel"
|
|
55
|
+
Issues = "https://github.com/arieradle/shekel/issues"
|
|
56
|
+
Changelog = "https://github.com/arieradle/shekel/blob/main/CHANGELOG.md"
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
asyncio_mode = "auto"
|
|
61
|
+
|
|
62
|
+
[tool.black]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = ["py39"]
|
|
65
|
+
|
|
66
|
+
[tool.isort]
|
|
67
|
+
profile = "black"
|
|
68
|
+
line_length = 100
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py39"
|
|
72
|
+
line-length = 100
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "I", "UP"]
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
python_version = "3.9"
|
|
79
|
+
strict = true
|
|
80
|
+
warn_return_any = true
|
|
81
|
+
warn_unused_configs = true
|