llm-agent-metering 0.3.1__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.
Potentially problematic release.
This version of llm-agent-metering might be problematic. Click here for more details.
- llm_agent_metering-0.3.1/LICENSE +21 -0
- llm_agent_metering-0.3.1/MANIFEST.in +4 -0
- llm_agent_metering-0.3.1/PKG-INFO +206 -0
- llm_agent_metering-0.3.1/README.md +167 -0
- llm_agent_metering-0.3.1/agent_metering/__init__.py +58 -0
- llm_agent_metering-0.3.1/agent_metering/__main__.py +5 -0
- llm_agent_metering-0.3.1/agent_metering/alerts.py +81 -0
- llm_agent_metering-0.3.1/agent_metering/autoload.py +21 -0
- llm_agent_metering-0.3.1/agent_metering/cli.py +343 -0
- llm_agent_metering-0.3.1/agent_metering/config.py +207 -0
- llm_agent_metering-0.3.1/agent_metering/context.py +62 -0
- llm_agent_metering-0.3.1/agent_metering/core.py +105 -0
- llm_agent_metering-0.3.1/agent_metering/frameworks.py +150 -0
- llm_agent_metering-0.3.1/agent_metering/instrument.py +232 -0
- llm_agent_metering-0.3.1/agent_metering/pricing.py +32 -0
- llm_agent_metering-0.3.1/agent_metering/providers/__init__.py +23 -0
- llm_agent_metering-0.3.1/agent_metering/providers/extractors.py +131 -0
- llm_agent_metering-0.3.1/agent_metering/providers/registry.py +151 -0
- llm_agent_metering-0.3.1/agent_metering/providers.yaml +14 -0
- llm_agent_metering-0.3.1/agent_metering/proxy.py +399 -0
- llm_agent_metering-0.3.1/agent_metering/storage.py +152 -0
- llm_agent_metering-0.3.1/agent_metering/user_detect.py +171 -0
- llm_agent_metering-0.3.1/agent_metering/vertex_auth.py +82 -0
- llm_agent_metering-0.3.1/agent_metering.pth +1 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/PKG-INFO +206 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/SOURCES.txt +41 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/dependency_links.txt +1 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/entry_points.txt +2 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/requires.txt +19 -0
- llm_agent_metering-0.3.1/llm_agent_metering.egg-info/top_level.txt +1 -0
- llm_agent_metering-0.3.1/pyproject.toml +64 -0
- llm_agent_metering-0.3.1/setup.cfg +4 -0
- llm_agent_metering-0.3.1/setup.py +26 -0
- llm_agent_metering-0.3.1/tests/test_alerts.py +56 -0
- llm_agent_metering-0.3.1/tests/test_cli.py +90 -0
- llm_agent_metering-0.3.1/tests/test_config.py +95 -0
- llm_agent_metering-0.3.1/tests/test_instrument.py +254 -0
- llm_agent_metering-0.3.1/tests/test_pricing.py +19 -0
- llm_agent_metering-0.3.1/tests/test_providers.py +106 -0
- llm_agent_metering-0.3.1/tests/test_proxy.py +531 -0
- llm_agent_metering-0.3.1/tests/test_storage.py +61 -0
- llm_agent_metering-0.3.1/tests/test_user_detect.py +211 -0
- llm_agent_metering-0.3.1/tests/test_vertex_auth.py +70 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prantakhandaker
|
|
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,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-agent-metering
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Language-agnostic LLM cost metering via HTTP proxy — track spend per customer and feature
|
|
5
|
+
Author: prantakhandaker
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/prantakhandaker/agent_metering
|
|
8
|
+
Project-URL: Repository, https://github.com/prantakhandaker/agent_metering
|
|
9
|
+
Project-URL: Issues, https://github.com/prantakhandaker/agent_metering/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/prantakhandaker/agent_metering/releases
|
|
11
|
+
Keywords: llm,openai,anthropic,cost-tracking,metering,observability,saas
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: fastapi>=0.100.0
|
|
24
|
+
Requires-Dist: uvicorn>=0.23.0
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
27
|
+
Provides-Extra: dashboard
|
|
28
|
+
Requires-Dist: streamlit>=1.28.0; extra == "dashboard"
|
|
29
|
+
Requires-Dist: pandas>=2.0.0; extra == "dashboard"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
33
|
+
Provides-Extra: example
|
|
34
|
+
Requires-Dist: openai>=1.0.0; extra == "example"
|
|
35
|
+
Requires-Dist: anthropic>=0.25.0; extra == "example"
|
|
36
|
+
Provides-Extra: vertex
|
|
37
|
+
Requires-Dist: google-auth>=2.22.0; extra == "vertex"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# agent_metering
|
|
41
|
+
|
|
42
|
+
[](https://github.com/prantakhandaker/agent_metering/actions/workflows/ci.yml)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://www.python.org/downloads/)
|
|
45
|
+
|
|
46
|
+
**Language-agnostic LLM cost metering** for B2B SaaS — point any OpenAI / Anthropic client at the HTTP proxy (Node, Go, Java, PHP, Python, curl, …).
|
|
47
|
+
|
|
48
|
+
**MIT open source** — [CONTRIBUTING](CONTRIBUTING.md) · [SECURITY](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install llm-agent-metering
|
|
54
|
+
# optional extras:
|
|
55
|
+
# pip install "llm-agent-metering[dashboard]"
|
|
56
|
+
# pip install "llm-agent-metering[example]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From GitHub (latest main):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "git+https://github.com/prantakhandaker/agent_metering.git"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Any language (recommended)
|
|
66
|
+
|
|
67
|
+
Run the proxy once, then set your SDK **base URL** to it. No SDK install in the app language required.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install llm-agent-metering
|
|
71
|
+
python -m uvicorn agent_metering.proxy:app --host 0.0.0.0 --port 8787
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Provider | Base URL / env |
|
|
75
|
+
|----------|----------------|
|
|
76
|
+
| OpenAI | `http://127.0.0.1:8787/proxy/openai/v1` → `OPENAI_BASE_URL` |
|
|
77
|
+
| Anthropic | `http://127.0.0.1:8787/proxy/anthropic` → `ANTHROPIC_BASE_URL` |
|
|
78
|
+
| Azure | `.../proxy/azure/v1` → `AZURE_OPENAI_BASE_URL` |
|
|
79
|
+
|
|
80
|
+
Optional per-user / feature headers (stripped before upstream):
|
|
81
|
+
|
|
82
|
+
- `X-User-Id` (preferred) or `X-Customer-Id`
|
|
83
|
+
- `X-Feature`
|
|
84
|
+
- Or OpenAI body field `user` / Anthropic `metadata.user_id`
|
|
85
|
+
|
|
86
|
+
Defaults: env `AGENT_METERING_CUSTOMER_ID` / `AGENT_METERING_FEATURE`, else `default`.
|
|
87
|
+
|
|
88
|
+
Spend → local SQLite `agent_metering.db`. Dashboard:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m streamlit run examples/dashboard.py # pip install "llm-agent-metering[dashboard]"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Node
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import OpenAI from "openai";
|
|
98
|
+
|
|
99
|
+
const client = new OpenAI({
|
|
100
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
101
|
+
baseURL: "http://127.0.0.1:8787/proxy/openai/v1",
|
|
102
|
+
defaultHeaders: { "X-User-Id": "user_42" },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await client.chat.completions.create({
|
|
106
|
+
model: "gpt-4o-mini",
|
|
107
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Full script: [`examples/proxy_node_example.mjs`](examples/proxy_node_example.mjs).
|
|
112
|
+
|
|
113
|
+
### curl
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl http://127.0.0.1:8787/proxy/openai/v1/chat/completions \
|
|
117
|
+
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
118
|
+
-H "Content-Type: application/json" \
|
|
119
|
+
-H "X-User-Id: user_42" \
|
|
120
|
+
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hi"}]}'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Go
|
|
124
|
+
|
|
125
|
+
```go
|
|
126
|
+
client := openai.NewClient(
|
|
127
|
+
option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
|
|
128
|
+
option.WithBaseURL("http://127.0.0.1:8787/proxy/openai/v1"),
|
|
129
|
+
option.WithHeader("X-User-Id", "user_42"),
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Docker sidecar
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
docker compose -f examples/docker-compose.sidecar.yml up --build
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
App containers only need `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` pointing at `http://metering-proxy:8787/proxy/...`.
|
|
140
|
+
|
|
141
|
+
Or wrap a local process:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
python -m agent_metering run --start-proxy -- python your_app.py
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Python-only shortcut (optional)
|
|
148
|
+
|
|
149
|
+
Same venv install auto-patches OpenAI / Anthropic SDKs (no base URL change):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install llm-agent-metering
|
|
153
|
+
# run your Python app — no import required
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Opt out: `AGENT_METERING_AUTO=0`. Demo: `python examples/auto_instrument_example.py`.
|
|
157
|
+
|
|
158
|
+
Also detects FastAPI/Flask/Django request users and OpenAI `user=` when present.
|
|
159
|
+
|
|
160
|
+
## Who this is for / not for
|
|
161
|
+
|
|
162
|
+
**For:** Any stack that can set an LLM HTTP base URL (or env) and needs **per-customer / per-user / per-feature** spend.
|
|
163
|
+
|
|
164
|
+
**Not for:** Full tracing/evals (Langfuse), or replacing multi-provider gateways you already run (LiteLLM / Portkey) unless you put this proxy in front.
|
|
165
|
+
|
|
166
|
+
## Why
|
|
167
|
+
|
|
168
|
+
Flat API rate limits do not protect margin. Agent workloads are open-ended: tool loops and long contexts can burn tokens quietly. Metering per customer/feature surfaces that before margin disappears.
|
|
169
|
+
|
|
170
|
+
## Project layout
|
|
171
|
+
|
|
172
|
+
**Primary (any language):** `proxy.py`, `providers/`, `cli.py`
|
|
173
|
+
**Python convenience:** `autoload.py` (`.pth`), `instrument.py`, `user_detect.py`, `frameworks.py`
|
|
174
|
+
**Shared:** `config.py`, `core.py`, `storage.py`, `context.py`
|
|
175
|
+
|
|
176
|
+
## Tests
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pytest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Releasing
|
|
183
|
+
|
|
184
|
+
Maintainers publish to PyPI via GitHub Actions Trusted Publishing (no API token in secrets).
|
|
185
|
+
|
|
186
|
+
1. One-time on [pypi.org](https://pypi.org): **Publishing → Pending publisher**
|
|
187
|
+
- Project: `llm-agent-metering`
|
|
188
|
+
- Owner: `prantakhandaker`
|
|
189
|
+
- Repository: `agent_metering`
|
|
190
|
+
- Workflow: `publish.yml`
|
|
191
|
+
- Environment: **leave empty**
|
|
192
|
+
2. Bump `version` in `pyproject.toml` to match the release tag.
|
|
193
|
+
3. Tag and release:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# version in pyproject.toml must match the tag
|
|
197
|
+
git tag v0.3.1
|
|
198
|
+
git push origin v0.3.1
|
|
199
|
+
# then GitHub → Releases → Draft release from that tag → Publish
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Publishing workflow: [`.github/workflows/publish.yml`](.github/workflows/publish.yml).
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# agent_metering
|
|
2
|
+
|
|
3
|
+
[](https://github.com/prantakhandaker/agent_metering/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
**Language-agnostic LLM cost metering** for B2B SaaS — point any OpenAI / Anthropic client at the HTTP proxy (Node, Go, Java, PHP, Python, curl, …).
|
|
8
|
+
|
|
9
|
+
**MIT open source** — [CONTRIBUTING](CONTRIBUTING.md) · [SECURITY](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install llm-agent-metering
|
|
15
|
+
# optional extras:
|
|
16
|
+
# pip install "llm-agent-metering[dashboard]"
|
|
17
|
+
# pip install "llm-agent-metering[example]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
From GitHub (latest main):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "git+https://github.com/prantakhandaker/agent_metering.git"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Any language (recommended)
|
|
27
|
+
|
|
28
|
+
Run the proxy once, then set your SDK **base URL** to it. No SDK install in the app language required.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install llm-agent-metering
|
|
32
|
+
python -m uvicorn agent_metering.proxy:app --host 0.0.0.0 --port 8787
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Provider | Base URL / env |
|
|
36
|
+
|----------|----------------|
|
|
37
|
+
| OpenAI | `http://127.0.0.1:8787/proxy/openai/v1` → `OPENAI_BASE_URL` |
|
|
38
|
+
| Anthropic | `http://127.0.0.1:8787/proxy/anthropic` → `ANTHROPIC_BASE_URL` |
|
|
39
|
+
| Azure | `.../proxy/azure/v1` → `AZURE_OPENAI_BASE_URL` |
|
|
40
|
+
|
|
41
|
+
Optional per-user / feature headers (stripped before upstream):
|
|
42
|
+
|
|
43
|
+
- `X-User-Id` (preferred) or `X-Customer-Id`
|
|
44
|
+
- `X-Feature`
|
|
45
|
+
- Or OpenAI body field `user` / Anthropic `metadata.user_id`
|
|
46
|
+
|
|
47
|
+
Defaults: env `AGENT_METERING_CUSTOMER_ID` / `AGENT_METERING_FEATURE`, else `default`.
|
|
48
|
+
|
|
49
|
+
Spend → local SQLite `agent_metering.db`. Dashboard:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m streamlit run examples/dashboard.py # pip install "llm-agent-metering[dashboard]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Node
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import OpenAI from "openai";
|
|
59
|
+
|
|
60
|
+
const client = new OpenAI({
|
|
61
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
62
|
+
baseURL: "http://127.0.0.1:8787/proxy/openai/v1",
|
|
63
|
+
defaultHeaders: { "X-User-Id": "user_42" },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await client.chat.completions.create({
|
|
67
|
+
model: "gpt-4o-mini",
|
|
68
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Full script: [`examples/proxy_node_example.mjs`](examples/proxy_node_example.mjs).
|
|
73
|
+
|
|
74
|
+
### curl
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl http://127.0.0.1:8787/proxy/openai/v1/chat/completions \
|
|
78
|
+
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-H "X-User-Id: user_42" \
|
|
81
|
+
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hi"}]}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Go
|
|
85
|
+
|
|
86
|
+
```go
|
|
87
|
+
client := openai.NewClient(
|
|
88
|
+
option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
|
|
89
|
+
option.WithBaseURL("http://127.0.0.1:8787/proxy/openai/v1"),
|
|
90
|
+
option.WithHeader("X-User-Id", "user_42"),
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Docker sidecar
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
docker compose -f examples/docker-compose.sidecar.yml up --build
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
App containers only need `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` pointing at `http://metering-proxy:8787/proxy/...`.
|
|
101
|
+
|
|
102
|
+
Or wrap a local process:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m agent_metering run --start-proxy -- python your_app.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Python-only shortcut (optional)
|
|
109
|
+
|
|
110
|
+
Same venv install auto-patches OpenAI / Anthropic SDKs (no base URL change):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install llm-agent-metering
|
|
114
|
+
# run your Python app — no import required
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Opt out: `AGENT_METERING_AUTO=0`. Demo: `python examples/auto_instrument_example.py`.
|
|
118
|
+
|
|
119
|
+
Also detects FastAPI/Flask/Django request users and OpenAI `user=` when present.
|
|
120
|
+
|
|
121
|
+
## Who this is for / not for
|
|
122
|
+
|
|
123
|
+
**For:** Any stack that can set an LLM HTTP base URL (or env) and needs **per-customer / per-user / per-feature** spend.
|
|
124
|
+
|
|
125
|
+
**Not for:** Full tracing/evals (Langfuse), or replacing multi-provider gateways you already run (LiteLLM / Portkey) unless you put this proxy in front.
|
|
126
|
+
|
|
127
|
+
## Why
|
|
128
|
+
|
|
129
|
+
Flat API rate limits do not protect margin. Agent workloads are open-ended: tool loops and long contexts can burn tokens quietly. Metering per customer/feature surfaces that before margin disappears.
|
|
130
|
+
|
|
131
|
+
## Project layout
|
|
132
|
+
|
|
133
|
+
**Primary (any language):** `proxy.py`, `providers/`, `cli.py`
|
|
134
|
+
**Python convenience:** `autoload.py` (`.pth`), `instrument.py`, `user_detect.py`, `frameworks.py`
|
|
135
|
+
**Shared:** `config.py`, `core.py`, `storage.py`, `context.py`
|
|
136
|
+
|
|
137
|
+
## Tests
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Releasing
|
|
144
|
+
|
|
145
|
+
Maintainers publish to PyPI via GitHub Actions Trusted Publishing (no API token in secrets).
|
|
146
|
+
|
|
147
|
+
1. One-time on [pypi.org](https://pypi.org): **Publishing → Pending publisher**
|
|
148
|
+
- Project: `llm-agent-metering`
|
|
149
|
+
- Owner: `prantakhandaker`
|
|
150
|
+
- Repository: `agent_metering`
|
|
151
|
+
- Workflow: `publish.yml`
|
|
152
|
+
- Environment: **leave empty**
|
|
153
|
+
2. Bump `version` in `pyproject.toml` to match the release tag.
|
|
154
|
+
3. Tag and release:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# version in pyproject.toml must match the tag
|
|
158
|
+
git tag v0.3.1
|
|
159
|
+
git push origin v0.3.1
|
|
160
|
+
# then GitHub → Releases → Draft release from that tag → Publish
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Publishing workflow: [`.github/workflows/publish.yml`](.github/workflows/publish.yml).
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""agent_metering — lightweight LLM cost tracking for B2B AI agents.
|
|
2
|
+
|
|
3
|
+
Install-only (site .pth) or import-once::
|
|
4
|
+
|
|
5
|
+
pip install agent-metering
|
|
6
|
+
# or: import agent_metering
|
|
7
|
+
|
|
8
|
+
agent_metering.set_user("cust_123") # optional, once per request
|
|
9
|
+
|
|
10
|
+
Existing OpenAI / Anthropic calls are metered automatically.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from agent_metering.alerts import check_budgets, slack_notifier
|
|
14
|
+
from agent_metering.context import (
|
|
15
|
+
clear_feature,
|
|
16
|
+
clear_user,
|
|
17
|
+
get_feature,
|
|
18
|
+
get_user,
|
|
19
|
+
set_feature,
|
|
20
|
+
set_user,
|
|
21
|
+
user_context,
|
|
22
|
+
)
|
|
23
|
+
from agent_metering.core import Meter
|
|
24
|
+
from agent_metering.instrument import disable, enable, get_meter, is_enabled, maybe_auto_enable
|
|
25
|
+
from agent_metering.storage import BaseStorage, SQLiteStorage, UsageRecord
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Meter",
|
|
29
|
+
"SQLiteStorage",
|
|
30
|
+
"BaseStorage",
|
|
31
|
+
"UsageRecord",
|
|
32
|
+
"check_budgets",
|
|
33
|
+
"slack_notifier",
|
|
34
|
+
"app",
|
|
35
|
+
"enable",
|
|
36
|
+
"disable",
|
|
37
|
+
"is_enabled",
|
|
38
|
+
"get_meter",
|
|
39
|
+
"set_user",
|
|
40
|
+
"get_user",
|
|
41
|
+
"clear_user",
|
|
42
|
+
"set_feature",
|
|
43
|
+
"get_feature",
|
|
44
|
+
"clear_feature",
|
|
45
|
+
"user_context",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __getattr__(name: str):
|
|
50
|
+
if name == "app":
|
|
51
|
+
from agent_metering.proxy import app as proxy_app
|
|
52
|
+
|
|
53
|
+
return proxy_app
|
|
54
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Auto-enable when config JSON is present (or AGENT_METERING_AUTO=1).
|
|
58
|
+
maybe_auto_enable()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Budget checks and Slack alerting helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any, Callable, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_budgets(
|
|
13
|
+
meter: Any,
|
|
14
|
+
customer_limits: Optional[dict[str, float]] = None,
|
|
15
|
+
feature_limits: Optional[dict[str, float]] = None,
|
|
16
|
+
window_seconds: float = 86400,
|
|
17
|
+
on_breach: Optional[Callable[[str, str, float, float], None]] = None,
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
"""Check spend within a rolling window against customer/feature limits.
|
|
20
|
+
|
|
21
|
+
Returns a list of breach dicts:
|
|
22
|
+
``{"scope": "customer"|"feature", "id": ..., "spent": ..., "limit": ...}``
|
|
23
|
+
"""
|
|
24
|
+
since_ts = time.time() - window_seconds
|
|
25
|
+
breaches: list[dict[str, Any]] = []
|
|
26
|
+
|
|
27
|
+
if customer_limits:
|
|
28
|
+
by_customer = meter.cost_by_customer(since_ts=since_ts)
|
|
29
|
+
for customer_id, limit in customer_limits.items():
|
|
30
|
+
spent = float(by_customer.get(customer_id, {}).get("total_cost_usd", 0.0))
|
|
31
|
+
if spent > limit:
|
|
32
|
+
breach = {
|
|
33
|
+
"scope": "customer",
|
|
34
|
+
"id": customer_id,
|
|
35
|
+
"spent": spent,
|
|
36
|
+
"limit": limit,
|
|
37
|
+
}
|
|
38
|
+
breaches.append(breach)
|
|
39
|
+
if on_breach is not None:
|
|
40
|
+
on_breach("customer", customer_id, spent, limit)
|
|
41
|
+
|
|
42
|
+
if feature_limits:
|
|
43
|
+
by_feature = meter.cost_by_feature(since_ts=since_ts)
|
|
44
|
+
for feature, limit in feature_limits.items():
|
|
45
|
+
spent = float(by_feature.get(feature, {}).get("total_cost_usd", 0.0))
|
|
46
|
+
if spent > limit:
|
|
47
|
+
breach = {
|
|
48
|
+
"scope": "feature",
|
|
49
|
+
"id": feature,
|
|
50
|
+
"spent": spent,
|
|
51
|
+
"limit": limit,
|
|
52
|
+
}
|
|
53
|
+
breaches.append(breach)
|
|
54
|
+
if on_breach is not None:
|
|
55
|
+
on_breach("feature", feature, spent, limit)
|
|
56
|
+
|
|
57
|
+
return breaches
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def slack_notifier(webhook_url: str) -> Callable[[str, str, float, float], None]:
|
|
61
|
+
"""Return an ``on_breach``-compatible function that POSTs to Slack."""
|
|
62
|
+
|
|
63
|
+
def _notify(scope: str, id_: str, spent: float, limit: float) -> None:
|
|
64
|
+
text = (
|
|
65
|
+
f":warning: Budget breach — {scope} `{id_}` spent "
|
|
66
|
+
f"${spent:.4f} (limit ${limit:.4f})"
|
|
67
|
+
)
|
|
68
|
+
payload = json.dumps({"text": text}).encode("utf-8")
|
|
69
|
+
req = urllib.request.Request(
|
|
70
|
+
webhook_url,
|
|
71
|
+
data=payload,
|
|
72
|
+
headers={"Content-Type": "application/json"},
|
|
73
|
+
method="POST",
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
77
|
+
resp.read()
|
|
78
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
79
|
+
print(f"slack_notifier error: {exc}")
|
|
80
|
+
|
|
81
|
+
return _notify
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Site .pth entry — enable metering without any app import.
|
|
2
|
+
|
|
3
|
+
Installed as ``agent_metering.pth`` in site-packages so every Python
|
|
4
|
+
process in that environment auto-instruments OpenAI / Anthropic SDKs.
|
|
5
|
+
Opt out with ``AGENT_METERING_AUTO=0``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _autoload() -> None:
|
|
12
|
+
try:
|
|
13
|
+
from agent_metering.instrument import maybe_auto_enable
|
|
14
|
+
|
|
15
|
+
maybe_auto_enable()
|
|
16
|
+
except Exception:
|
|
17
|
+
# Never break host interpreter startup.
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_autoload()
|