keymeter 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.
@@ -0,0 +1,26 @@
1
+ # secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.egg-info/
10
+ .venv/
11
+ venv/
12
+ build/
13
+ dist/
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # poll logs written with --log
20
+ *.csv
21
+
22
+ # editors / OS
23
+ .vscode/
24
+ .idea/
25
+ .DS_Store
26
+ Thumbs.db
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-07)
4
+
5
+ First public release.
6
+
7
+ - `keymeter web`: browser dashboard with spend vs. budget, burn rate, run-out time, projected spend at
8
+ reset, spend-over-time charts, spend per model, limits, an event log, and optional sound and desktop
9
+ alerts. Listens on localhost only unless `--host` says otherwise.
10
+ - `keymeter tui`: the same numbers as a live terminal dashboard, plus the team's budget and keys on
11
+ LiteLLM when the key may read them.
12
+ - `keymeter json`: one snapshot as JSON, exit code 1 when the key is not usable.
13
+ - Gateways: LiteLLM proxy (`/key/info`) and OpenRouter (`/api/v1/key`), picked automatically from the
14
+ URL or the key.
keymeter-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Do Pham Bao Hoang
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,214 @@
1
+ Metadata-Version: 2.5
2
+ Name: keymeter
3
+ Version: 0.1.0
4
+ Summary: Live spend, budget and burn-rate dashboard for one LiteLLM or OpenRouter API key, in the browser or the terminal.
5
+ Project-URL: Homepage, https://github.com/hoanghero125/keymeter
6
+ Project-URL: Issues, https://github.com/hoanghero125/keymeter/issues
7
+ Project-URL: Changelog, https://github.com/hoanghero125/keymeter/blob/main/CHANGELOG.md
8
+ Author: Do Pham Bao Hoang
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api-key,budget,dashboard,litellm,llm,monitoring,openrouter,quota,spend
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: System :: Monitoring
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: rich>=13
27
+ Description-Content-Type: text/markdown
28
+
29
+ # keymeter
30
+
31
+ [![CI](https://github.com/hoanghero125/keymeter/actions/workflows/ci.yml/badge.svg)](https://github.com/hoanghero125/keymeter/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/keymeter)](https://pypi.org/project/keymeter/)
33
+
34
+ A live dashboard for one LLM gateway API key: how much of its budget is spent, how fast it is going,
35
+ and when it will run out. It works with a [LiteLLM](https://github.com/BerriAI/litellm) proxy or
36
+ [OpenRouter](https://openrouter.ai), in the browser or in the terminal.
37
+
38
+ ![keymeter in the browser](https://raw.githubusercontent.com/hoanghero125/keymeter/main/docs/screenshot.png)
39
+
40
+ LiteLLM's admin UI needs an admin key. keymeter only needs the key you were handed, which is often all
41
+ you have: a team key at work, a key for a hackathon or a course. It polls the gateway's read-only key
42
+ endpoint (no tokens spent) and shows:
43
+
44
+ - spend vs. budget, what is left, and when the budget resets
45
+ - the burn rate over the last 15 minutes, when the budget runs out at that rate, and the projected
46
+ spend at reset
47
+ - charts of spend over time and spend per interval, with a table view
48
+ - spend per model and per-model budgets, rate limits, allowed models, expiry and blocked state (LiteLLM)
49
+ - an event log, and optional sound and desktop alerts when the status changes or a budget passes 80%
50
+ or 95%
51
+
52
+ The key stays in the keymeter process. The browser only ever sees a masked version of it.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pipx install keymeter # or: uv tool install keymeter
58
+ ```
59
+
60
+ keymeter needs Python 3.10 or newer.
61
+
62
+ ## Quick start
63
+
64
+ **LiteLLM proxy**
65
+
66
+ ```bash
67
+ export KEYMETER_URL=https://llm.example.com # your proxy
68
+ export KEYMETER_KEY=sk-...
69
+ keymeter web
70
+ ```
71
+
72
+ **OpenRouter**
73
+
74
+ ```bash
75
+ export KEYMETER_KEY=sk-or-v1-...
76
+ keymeter web
77
+ ```
78
+
79
+ Then open <http://localhost:8765>. keymeter recognises OpenRouter from the `sk-or-` key or an
80
+ `openrouter.ai` URL, so it needs no URL there.
81
+
82
+ On Windows PowerShell, set the variables with `$env:KEYMETER_KEY = "sk-..."`. You can also put
83
+ them in a `.env` file instead (see [Configuration](#configuration)).
84
+
85
+ ## Commands
86
+
87
+ | Command | |
88
+ |---|---|
89
+ | `keymeter web` | browser dashboard on <http://localhost:8765> |
90
+ | `keymeter tui` | live dashboard in the terminal; on LiteLLM it also shows the team's budget and keys when the key may read them |
91
+ | `keymeter tui --once` | print one snapshot and exit |
92
+ | `keymeter json` | print one snapshot as JSON and exit with code 1 when the key is not usable, for scripts and cron jobs |
93
+
94
+ Options (`keymeter COMMAND -h` lists them per command):
95
+
96
+ | Option | Default | |
97
+ |---|---|---|
98
+ | `--gateway` | `auto` | `litellm`, `openrouter`, or `auto` to pick from the URL and key |
99
+ | `--url` | depends on the gateway | gateway base URL; `http://localhost:4000` for LiteLLM, `https://openrouter.ai/api` for OpenRouter. A trailing `/v1` is fine. |
100
+ | `--env FILE` | `.env` | where to read settings from |
101
+ | `-i`, `--interval` | `15` | seconds between polls (`web`, `tui`) |
102
+ | `--window` | `15` | burn-rate window in minutes (`web`, `tui`) |
103
+ | `--warn` / `--crit` | `0.8` / `0.95` | alert thresholds, as a fraction of a budget (`web`, `tui`) |
104
+ | `--log FILE.csv` | | also append every poll to a CSV file (`web`, `tui`) |
105
+ | `--host` / `--port` | `127.0.0.1` / `8765` | where the dashboard listens (`web`) |
106
+ | `--no-team` | | skip LiteLLM's `/team/info` (`tui`, `json`) |
107
+ | `--no-bell` | | don't beep on alerts (`tui`) |
108
+
109
+ ## Configuration
110
+
111
+ keymeter reads three settings, from the environment first and then from a `.env` file in the
112
+ current directory (or the file given with `--env`):
113
+
114
+ | Variable | |
115
+ |---|---|
116
+ | `KEYMETER_KEY` | the API key to watch (required) |
117
+ | `KEYMETER_URL` | gateway base URL |
118
+ | `KEYMETER_GATEWAY` | `auto`, `litellm` or `openrouter` |
119
+
120
+ Copy [`.env.example`](.env.example) to `.env` to start. There is no `--key` option, because a key
121
+ typed on the command line ends up in your shell history.
122
+
123
+ When `KEYMETER_KEY` is already set in the environment, keymeter ignores `./.env` (but still reads a
124
+ file you name with `--env`). That way a `.env` in whatever directory you happen to be in, such as a
125
+ cloned repository, can't send your key to another server.
126
+
127
+ ## What each gateway reports
128
+
129
+ | | LiteLLM | OpenRouter |
130
+ |---|---|---|
131
+ | Spend, budget, reset time, burn rate, projections | yes | yes |
132
+ | Key expiry | yes | yes |
133
+ | Spend per model, per-model budgets | yes | no |
134
+ | Rate limits, allowed models, blocked state | yes | no |
135
+ | Team budget and team keys (`tui`, `json`) | yes | no |
136
+
137
+ On OpenRouter the budget is the key's credit limit. The spend shown is what counts against that limit
138
+ in the current period. Limits reset at 00:00 UTC: daily, weekly on Monday, or on the 1st of the month.
139
+ A key without a limit shows its all-time usage and no budget.
140
+
141
+ ## Running it on a server
142
+
143
+ `keymeter web` listens on `127.0.0.1` only. To watch it from your laptop, forward the port over SSH:
144
+
145
+ ```bash
146
+ # on the server
147
+ keymeter web
148
+
149
+ # on your laptop
150
+ ssh -N -L 8765:localhost:8765 you@server
151
+ ```
152
+
153
+ Then open <http://localhost:8765> on the laptop. Because the browser sees `localhost`, desktop
154
+ notifications work as well (browsers only allow them over HTTPS or on localhost).
155
+
156
+ On `127.0.0.1`, keymeter answers only requests addressed to `localhost`, so other websites can't read
157
+ it through DNS rebinding. A reverse proxy in front of it has to send `Host: localhost`.
158
+
159
+ To keep it running after you log out, use `tmux`, a systemd service, or
160
+ `nohup keymeter web > keymeter.log 2>&1 &`.
161
+
162
+ `keymeter web --host 0.0.0.0` serves the dashboard on every interface instead. There is no login,
163
+ so anyone who can reach the port can see the key's spend, budget and model usage (not the key
164
+ itself) and press "Poll now". If you do this, allow only your own IP in the firewall.
165
+
166
+ ## How it works
167
+
168
+ - keymeter calls `GET /key/info` on LiteLLM (plus `/team/info` for `tui` and `json`), or
169
+ `GET /api/v1/key` on OpenRouter. These endpoints are read-only and cost nothing.
170
+ - The burn rate is the spend added over the burn-rate window, per hour. When spend goes down,
171
+ keymeter takes it as a budget reset and starts measuring again.
172
+ - When the gateway stops answering, polls back off to at most once a minute. The dashboard keeps
173
+ showing the last numbers it got and says how old they are.
174
+ - The charts' history lives in memory for 24 hours and starts over when keymeter restarts. Use
175
+ `--log` to keep a permanent record.
176
+ - The bell button in the browser turns on alerts: a short sound, plus a desktop notification when
177
+ the browser allows it.
178
+ - The web UI fades things in and out as they change, and keeps still if your system asks for reduced
179
+ motion.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ git clone https://github.com/hoanghero125/keymeter
185
+ cd keymeter
186
+ uv sync
187
+ uv run pytest
188
+ uv run ruff check
189
+ ```
190
+
191
+ Without uv, run `python -m venv .venv`, activate it, then `pip install -e . --group dev` (pip 25.1
192
+ or newer).
193
+
194
+ The web UI is plain HTML, CSS and JavaScript in `src/keymeter/static`, with no build step.
195
+
196
+ To support another gateway, add an adapter in `src/keymeter/gateways/`. It fetches the key's data
197
+ and translates it into the field names the rest of keymeter uses (LiteLLM's); see `openrouter.py`
198
+ for a small example.
199
+
200
+ ### Releasing
201
+
202
+ 1. Set `__version__` in `src/keymeter/__init__.py` and add the release to `CHANGELOG.md`.
203
+ 2. Commit, then tag and push: `git tag v0.1.0 && git push origin v0.1.0`.
204
+
205
+ The `Release` workflow tests and builds the package, then publishes it to PyPI. Before the first
206
+ release, set up trusted publishing once:
207
+
208
+ - On PyPI, under *Your account → Publishing*, add a pending publisher with project `keymeter`,
209
+ owner `hoanghero125`, repository `keymeter`, workflow `release.yml` and environment `pypi`.
210
+ - On GitHub, under *Settings → Environments*, create an environment named `pypi`.
211
+
212
+ ## License
213
+
214
+ [MIT](LICENSE). keymeter is not affiliated with LiteLLM (BerriAI) or OpenRouter.
@@ -0,0 +1,186 @@
1
+ # keymeter
2
+
3
+ [![CI](https://github.com/hoanghero125/keymeter/actions/workflows/ci.yml/badge.svg)](https://github.com/hoanghero125/keymeter/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/keymeter)](https://pypi.org/project/keymeter/)
5
+
6
+ A live dashboard for one LLM gateway API key: how much of its budget is spent, how fast it is going,
7
+ and when it will run out. It works with a [LiteLLM](https://github.com/BerriAI/litellm) proxy or
8
+ [OpenRouter](https://openrouter.ai), in the browser or in the terminal.
9
+
10
+ ![keymeter in the browser](https://raw.githubusercontent.com/hoanghero125/keymeter/main/docs/screenshot.png)
11
+
12
+ LiteLLM's admin UI needs an admin key. keymeter only needs the key you were handed, which is often all
13
+ you have: a team key at work, a key for a hackathon or a course. It polls the gateway's read-only key
14
+ endpoint (no tokens spent) and shows:
15
+
16
+ - spend vs. budget, what is left, and when the budget resets
17
+ - the burn rate over the last 15 minutes, when the budget runs out at that rate, and the projected
18
+ spend at reset
19
+ - charts of spend over time and spend per interval, with a table view
20
+ - spend per model and per-model budgets, rate limits, allowed models, expiry and blocked state (LiteLLM)
21
+ - an event log, and optional sound and desktop alerts when the status changes or a budget passes 80%
22
+ or 95%
23
+
24
+ The key stays in the keymeter process. The browser only ever sees a masked version of it.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pipx install keymeter # or: uv tool install keymeter
30
+ ```
31
+
32
+ keymeter needs Python 3.10 or newer.
33
+
34
+ ## Quick start
35
+
36
+ **LiteLLM proxy**
37
+
38
+ ```bash
39
+ export KEYMETER_URL=https://llm.example.com # your proxy
40
+ export KEYMETER_KEY=sk-...
41
+ keymeter web
42
+ ```
43
+
44
+ **OpenRouter**
45
+
46
+ ```bash
47
+ export KEYMETER_KEY=sk-or-v1-...
48
+ keymeter web
49
+ ```
50
+
51
+ Then open <http://localhost:8765>. keymeter recognises OpenRouter from the `sk-or-` key or an
52
+ `openrouter.ai` URL, so it needs no URL there.
53
+
54
+ On Windows PowerShell, set the variables with `$env:KEYMETER_KEY = "sk-..."`. You can also put
55
+ them in a `.env` file instead (see [Configuration](#configuration)).
56
+
57
+ ## Commands
58
+
59
+ | Command | |
60
+ |---|---|
61
+ | `keymeter web` | browser dashboard on <http://localhost:8765> |
62
+ | `keymeter tui` | live dashboard in the terminal; on LiteLLM it also shows the team's budget and keys when the key may read them |
63
+ | `keymeter tui --once` | print one snapshot and exit |
64
+ | `keymeter json` | print one snapshot as JSON and exit with code 1 when the key is not usable, for scripts and cron jobs |
65
+
66
+ Options (`keymeter COMMAND -h` lists them per command):
67
+
68
+ | Option | Default | |
69
+ |---|---|---|
70
+ | `--gateway` | `auto` | `litellm`, `openrouter`, or `auto` to pick from the URL and key |
71
+ | `--url` | depends on the gateway | gateway base URL; `http://localhost:4000` for LiteLLM, `https://openrouter.ai/api` for OpenRouter. A trailing `/v1` is fine. |
72
+ | `--env FILE` | `.env` | where to read settings from |
73
+ | `-i`, `--interval` | `15` | seconds between polls (`web`, `tui`) |
74
+ | `--window` | `15` | burn-rate window in minutes (`web`, `tui`) |
75
+ | `--warn` / `--crit` | `0.8` / `0.95` | alert thresholds, as a fraction of a budget (`web`, `tui`) |
76
+ | `--log FILE.csv` | | also append every poll to a CSV file (`web`, `tui`) |
77
+ | `--host` / `--port` | `127.0.0.1` / `8765` | where the dashboard listens (`web`) |
78
+ | `--no-team` | | skip LiteLLM's `/team/info` (`tui`, `json`) |
79
+ | `--no-bell` | | don't beep on alerts (`tui`) |
80
+
81
+ ## Configuration
82
+
83
+ keymeter reads three settings, from the environment first and then from a `.env` file in the
84
+ current directory (or the file given with `--env`):
85
+
86
+ | Variable | |
87
+ |---|---|
88
+ | `KEYMETER_KEY` | the API key to watch (required) |
89
+ | `KEYMETER_URL` | gateway base URL |
90
+ | `KEYMETER_GATEWAY` | `auto`, `litellm` or `openrouter` |
91
+
92
+ Copy [`.env.example`](.env.example) to `.env` to start. There is no `--key` option, because a key
93
+ typed on the command line ends up in your shell history.
94
+
95
+ When `KEYMETER_KEY` is already set in the environment, keymeter ignores `./.env` (but still reads a
96
+ file you name with `--env`). That way a `.env` in whatever directory you happen to be in, such as a
97
+ cloned repository, can't send your key to another server.
98
+
99
+ ## What each gateway reports
100
+
101
+ | | LiteLLM | OpenRouter |
102
+ |---|---|---|
103
+ | Spend, budget, reset time, burn rate, projections | yes | yes |
104
+ | Key expiry | yes | yes |
105
+ | Spend per model, per-model budgets | yes | no |
106
+ | Rate limits, allowed models, blocked state | yes | no |
107
+ | Team budget and team keys (`tui`, `json`) | yes | no |
108
+
109
+ On OpenRouter the budget is the key's credit limit. The spend shown is what counts against that limit
110
+ in the current period. Limits reset at 00:00 UTC: daily, weekly on Monday, or on the 1st of the month.
111
+ A key without a limit shows its all-time usage and no budget.
112
+
113
+ ## Running it on a server
114
+
115
+ `keymeter web` listens on `127.0.0.1` only. To watch it from your laptop, forward the port over SSH:
116
+
117
+ ```bash
118
+ # on the server
119
+ keymeter web
120
+
121
+ # on your laptop
122
+ ssh -N -L 8765:localhost:8765 you@server
123
+ ```
124
+
125
+ Then open <http://localhost:8765> on the laptop. Because the browser sees `localhost`, desktop
126
+ notifications work as well (browsers only allow them over HTTPS or on localhost).
127
+
128
+ On `127.0.0.1`, keymeter answers only requests addressed to `localhost`, so other websites can't read
129
+ it through DNS rebinding. A reverse proxy in front of it has to send `Host: localhost`.
130
+
131
+ To keep it running after you log out, use `tmux`, a systemd service, or
132
+ `nohup keymeter web > keymeter.log 2>&1 &`.
133
+
134
+ `keymeter web --host 0.0.0.0` serves the dashboard on every interface instead. There is no login,
135
+ so anyone who can reach the port can see the key's spend, budget and model usage (not the key
136
+ itself) and press "Poll now". If you do this, allow only your own IP in the firewall.
137
+
138
+ ## How it works
139
+
140
+ - keymeter calls `GET /key/info` on LiteLLM (plus `/team/info` for `tui` and `json`), or
141
+ `GET /api/v1/key` on OpenRouter. These endpoints are read-only and cost nothing.
142
+ - The burn rate is the spend added over the burn-rate window, per hour. When spend goes down,
143
+ keymeter takes it as a budget reset and starts measuring again.
144
+ - When the gateway stops answering, polls back off to at most once a minute. The dashboard keeps
145
+ showing the last numbers it got and says how old they are.
146
+ - The charts' history lives in memory for 24 hours and starts over when keymeter restarts. Use
147
+ `--log` to keep a permanent record.
148
+ - The bell button in the browser turns on alerts: a short sound, plus a desktop notification when
149
+ the browser allows it.
150
+ - The web UI fades things in and out as they change, and keeps still if your system asks for reduced
151
+ motion.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ git clone https://github.com/hoanghero125/keymeter
157
+ cd keymeter
158
+ uv sync
159
+ uv run pytest
160
+ uv run ruff check
161
+ ```
162
+
163
+ Without uv, run `python -m venv .venv`, activate it, then `pip install -e . --group dev` (pip 25.1
164
+ or newer).
165
+
166
+ The web UI is plain HTML, CSS and JavaScript in `src/keymeter/static`, with no build step.
167
+
168
+ To support another gateway, add an adapter in `src/keymeter/gateways/`. It fetches the key's data
169
+ and translates it into the field names the rest of keymeter uses (LiteLLM's); see `openrouter.py`
170
+ for a small example.
171
+
172
+ ### Releasing
173
+
174
+ 1. Set `__version__` in `src/keymeter/__init__.py` and add the release to `CHANGELOG.md`.
175
+ 2. Commit, then tag and push: `git tag v0.1.0 && git push origin v0.1.0`.
176
+
177
+ The `Release` workflow tests and builds the package, then publishes it to PyPI. Before the first
178
+ release, set up trusted publishing once:
179
+
180
+ - On PyPI, under *Your account → Publishing*, add a pending publisher with project `keymeter`,
181
+ owner `hoanghero125`, repository `keymeter`, workflow `release.yml` and environment `pypi`.
182
+ - On GitHub, under *Settings → Environments*, create an environment named `pypi`.
183
+
184
+ ## License
185
+
186
+ [MIT](LICENSE). keymeter is not affiliated with LiteLLM (BerriAI) or OpenRouter.
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "keymeter"
7
+ dynamic = ["version"]
8
+ description = "Live spend, budget and burn-rate dashboard for one LiteLLM or OpenRouter API key, in the browser or the terminal."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Do Pham Bao Hoang" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["rich>=13"]
15
+ keywords = ["litellm", "openrouter", "llm", "api-key", "budget", "quota", "spend", "dashboard", "monitoring"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Environment :: Web Environment",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: System :: Monitoring",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/hoanghero125/keymeter"
34
+ Issues = "https://github.com/hoanghero125/keymeter/issues"
35
+ Changelog = "https://github.com/hoanghero125/keymeter/blob/main/CHANGELOG.md"
36
+
37
+ [project.scripts]
38
+ keymeter = "keymeter.cli:main"
39
+
40
+ [dependency-groups]
41
+ dev = ["pytest>=8", "ruff>=0.6"]
42
+
43
+ [tool.hatch.version]
44
+ path = "src/keymeter/__init__.py"
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["src", "tests", "CHANGELOG.md"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+
52
+ [tool.ruff]
53
+ line-length = 140
54
+ target-version = "py310"
55
+
56
+ [tool.ruff.lint]
57
+ extend-select = ["I", "B", "UP"]
@@ -0,0 +1,3 @@
1
+ """keymeter: live spend, budget and burn rate for one LiteLLM or OpenRouter API key."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from keymeter.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,114 @@
1
+ """Command line: `keymeter web`, `keymeter tui` and `keymeter json`.
2
+
3
+ Settings come from the environment, then from a .env file (the current directory's, or --env FILE):
4
+ KEYMETER_KEY the API key to watch (required)
5
+ KEYMETER_URL gateway base URL (default depends on the gateway)
6
+ KEYMETER_GATEWAY auto, litellm or openrouter (default auto)
7
+ """
8
+ import argparse
9
+ import json
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from keymeter import __version__, gateways
15
+ from keymeter.monitor import Monitor
16
+ from keymeter.util import tidy
17
+
18
+
19
+ def load_env(path, required):
20
+ """Minimal .env reader (KEY=VALUE lines); real environment variables win."""
21
+ if not path.is_file():
22
+ if required:
23
+ sys.exit(f"--env file not found: {path}")
24
+ return
25
+ raw = path.read_bytes()
26
+ try:
27
+ text = raw.decode("utf-16") if raw[:2] in (b"\xff\xfe", b"\xfe\xff") else raw.decode("utf-8-sig")
28
+ except UnicodeDecodeError:
29
+ sys.exit(f"can't read {path}: save it as UTF-8")
30
+ for line in text.splitlines():
31
+ line = line.strip()
32
+ if not line or line.startswith("#") or "=" not in line:
33
+ continue
34
+ k, v = line.split("=", 1)
35
+ os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
36
+
37
+
38
+ def build_parser():
39
+ conn = argparse.ArgumentParser(add_help=False)
40
+ g = conn.add_argument_group("gateway")
41
+ g.add_argument("--gateway", choices=["auto", *gateways.GATEWAYS],
42
+ help="gateway type (default: $KEYMETER_GATEWAY or auto: openrouter for openrouter.ai URLs "
43
+ "and sk-or- keys, litellm otherwise)")
44
+ g.add_argument("--url", help="gateway base URL (default: $KEYMETER_URL, else http://localhost:4000 for "
45
+ "litellm or https://openrouter.ai/api for openrouter)")
46
+ g.add_argument("--env", metavar="FILE", help="read settings from this file (default: .env in the current directory)")
47
+
48
+ watch = argparse.ArgumentParser(add_help=False)
49
+ w = watch.add_argument_group("monitoring")
50
+ w.add_argument("-i", "--interval", type=float, default=15, help="seconds between polls (default 15)")
51
+ w.add_argument("--window", type=float, default=15, help="burn-rate window in minutes (default 15)")
52
+ w.add_argument("--warn", type=float, default=0.8, help="warn at this fraction of a budget (default 0.8)")
53
+ w.add_argument("--crit", type=float, default=0.95, help="critical at this fraction (default 0.95)")
54
+ w.add_argument("--log", metavar="CSV", help="also append every poll to this CSV file")
55
+
56
+ p = argparse.ArgumentParser(
57
+ prog="keymeter",
58
+ description="Live spend, budget and burn rate for one LiteLLM or OpenRouter API key.",
59
+ epilog="The key is read from KEYMETER_KEY (environment or .env). Run `keymeter COMMAND -h` for a command's options.")
60
+ p.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
61
+ sub = p.add_subparsers(dest="command", required=True, metavar="COMMAND")
62
+
63
+ web = sub.add_parser("web", parents=[conn, watch], help="browser dashboard",
64
+ description="Serve a live dashboard for the key in the browser.")
65
+ web.add_argument("--host", default="127.0.0.1",
66
+ help="address to listen on (default 127.0.0.1, this machine only; 0.0.0.0 = all interfaces, no login)")
67
+ web.add_argument("--port", type=int, default=8765, help="port (default 8765)")
68
+
69
+ tui = sub.add_parser("tui", parents=[conn, watch], help="terminal dashboard",
70
+ description="Show a live dashboard for the key in the terminal.")
71
+ tui.add_argument("--once", action="store_true", help="print one snapshot and exit (exit 1 if not OK)")
72
+ tui.add_argument("--no-team", action="store_true", help="don't query the team (LiteLLM /team/info)")
73
+ tui.add_argument("--no-bell", action="store_true", help="don't beep on alerts")
74
+
75
+ js = sub.add_parser("json", parents=[conn], help="print one snapshot as JSON and exit",
76
+ description="Poll once and print the summary as JSON. Exits 1 if the status is not OK.")
77
+ js.add_argument("--no-team", action="store_true", help="don't query the team (LiteLLM /team/info)")
78
+ js.set_defaults(interval=15, window=15, warn=0.8, crit=0.95, log=None)
79
+ return p
80
+
81
+
82
+ def main(argv=None):
83
+ # write UTF-8 even when output goes to a pipe or file on Windows (Python's default from 3.15 on, PEP 686)
84
+ for stream in (sys.stdout, sys.stderr):
85
+ if hasattr(stream, "reconfigure"):
86
+ stream.reconfigure(encoding="utf-8")
87
+ args = build_parser().parse_args(argv)
88
+ args.interval = max(args.interval, 1)
89
+ if args.env:
90
+ load_env(Path(args.env), required=True)
91
+ elif not os.environ.get("KEYMETER_KEY"):
92
+ # ./.env is only a fallback for the key itself: a .env in whatever directory you are in (a cloned
93
+ # repo, say) must not point a key from your environment at another server
94
+ load_env(Path(".env"), required=False)
95
+ key = os.environ.get("KEYMETER_KEY", "").strip().strip('"')
96
+ if not key:
97
+ sys.exit("KEYMETER_KEY is not set. Put it in a .env file in this directory, pass --env FILE, or export it.")
98
+ name = (args.gateway or os.environ.get("KEYMETER_GATEWAY") or "auto").strip().lower()
99
+ team = args.command != "web" and not args.no_team # the web dashboard covers the key only
100
+ try:
101
+ gw = gateways.create(name, args.url or os.environ.get("KEYMETER_URL"), key, team=team)
102
+ except ValueError as e:
103
+ sys.exit(str(e))
104
+ mon = Monitor(gw, args)
105
+
106
+ if args.command == "json":
107
+ mon.poll()
108
+ print(json.dumps(tidy(mon.summary()), indent=2, ensure_ascii=False))
109
+ return 0 if mon.snap.status == "OK" else 1
110
+ if args.command == "web":
111
+ from keymeter import web
112
+ return web.serve(mon, args)
113
+ from keymeter import tui
114
+ return tui.run(mon, args)