codex-meter 0.3.0__py3-none-any.whl

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.
codex_meter/windows.py ADDED
@@ -0,0 +1,153 @@
1
+ """Rate-limit window math. Pure functions, no I/O."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ from dataclasses import dataclass
7
+
8
+ from codex_meter.models import RateLimitSample
9
+
10
+ BURN_RATE_LOOKBACK_HOURS = 6
11
+ MIN_BURN_RATE_SAMPLES = 3
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class WindowState:
16
+ """Decoded state of a single rate-limit window at a moment in time."""
17
+
18
+ window: str # "primary" or "secondary"
19
+ used_percent: float | None
20
+ window_minutes: int | None
21
+ reset_at: dt.datetime | None
22
+ seconds_remaining: int | None
23
+ burn_rate_per_hour: float | None # percent points per hour
24
+ eta_to_100: dt.datetime | None
25
+ samples: int
26
+
27
+
28
+ def _coerce_float(value: object) -> float | None:
29
+ try:
30
+ return float(value) if value is not None else None
31
+ except (TypeError, ValueError):
32
+ return None
33
+
34
+
35
+ def _coerce_int(value: object) -> int | None:
36
+ try:
37
+ return int(value) if value is not None else None
38
+ except (TypeError, ValueError):
39
+ return None
40
+
41
+
42
+ def _epoch_to_datetime(value: object) -> dt.datetime | None:
43
+ seconds = _coerce_int(value)
44
+ if seconds is None:
45
+ return None
46
+ try:
47
+ return dt.datetime.fromtimestamp(seconds, tz=dt.UTC)
48
+ except (OverflowError, OSError, ValueError):
49
+ return None
50
+
51
+
52
+ def _percent(sample: RateLimitSample, which: str) -> float | None:
53
+ return _coerce_float(getattr(sample, f"{which}_used_percent", None))
54
+
55
+
56
+ def _window_minutes(sample: RateLimitSample, which: str) -> int | None:
57
+ return _coerce_int(getattr(sample, f"{which}_window_minutes", None))
58
+
59
+
60
+ def _reset_at(sample: RateLimitSample, which: str) -> dt.datetime | None:
61
+ return _epoch_to_datetime(getattr(sample, f"{which}_resets_at", None))
62
+
63
+
64
+ def compute_window_state(
65
+ samples: list[RateLimitSample], now: dt.datetime, which: str
66
+ ) -> WindowState:
67
+ """Compute a WindowState from raw RateLimitSamples. `which` is 'primary' or 'secondary'."""
68
+ if which not in {"primary", "secondary"}:
69
+ raise ValueError(f"window must be 'primary' or 'secondary', got {which!r}")
70
+ if not samples:
71
+ return WindowState(
72
+ window=which,
73
+ used_percent=None,
74
+ window_minutes=None,
75
+ reset_at=None,
76
+ seconds_remaining=None,
77
+ burn_rate_per_hour=None,
78
+ eta_to_100=None,
79
+ samples=0,
80
+ )
81
+
82
+ ordered = sorted(samples, key=lambda sample: sample.timestamp)
83
+ latest = ordered[-1]
84
+ used = _percent(latest, which)
85
+ window_minutes = _window_minutes(latest, which)
86
+ reset_at = _reset_at(latest, which)
87
+
88
+ seconds_remaining: int | None = None
89
+ if reset_at is not None:
90
+ seconds_remaining = max(0, int((reset_at - now).total_seconds()))
91
+
92
+ burn_rate = _compute_burn_rate(ordered, latest, which)
93
+ eta_to_100 = _project_to_full(used, burn_rate, now)
94
+
95
+ return WindowState(
96
+ window=which,
97
+ used_percent=used,
98
+ window_minutes=window_minutes,
99
+ reset_at=reset_at,
100
+ seconds_remaining=seconds_remaining,
101
+ burn_rate_per_hour=burn_rate,
102
+ eta_to_100=eta_to_100,
103
+ samples=len(ordered),
104
+ )
105
+
106
+
107
+ def _compute_burn_rate(
108
+ ordered: list[RateLimitSample], latest: RateLimitSample, which: str
109
+ ) -> float | None:
110
+ """Burn rate in percent-points per hour, derived from samples within the lookback window."""
111
+ cutoff = latest.timestamp - dt.timedelta(hours=BURN_RATE_LOOKBACK_HOURS)
112
+ recent = [sample for sample in ordered if sample.timestamp >= cutoff]
113
+ if len(recent) < MIN_BURN_RATE_SAMPLES:
114
+ return None
115
+ first = recent[0]
116
+ first_pct = _percent(first, which)
117
+ last_pct = _percent(latest, which)
118
+ if first_pct is None or last_pct is None:
119
+ return None
120
+ elapsed_hours = (latest.timestamp - first.timestamp).total_seconds() / 3600.0
121
+ if elapsed_hours <= 0:
122
+ return None
123
+ return (last_pct - first_pct) / elapsed_hours
124
+
125
+
126
+ def _project_to_full(
127
+ used: float | None, burn_rate: float | None, now: dt.datetime
128
+ ) -> dt.datetime | None:
129
+ if used is None or burn_rate is None or burn_rate <= 0 or used >= 100:
130
+ return None
131
+ hours_to_full = (100.0 - used) / burn_rate
132
+ return now + dt.timedelta(hours=hours_to_full)
133
+
134
+
135
+ def format_seconds_remaining(seconds: int | None) -> str:
136
+ """Render seconds as `Xh YYm ZZs`, `Ym Ss`, or `—`."""
137
+ if seconds is None:
138
+ return "—"
139
+ seconds = max(0, int(seconds))
140
+ hours, rem = divmod(seconds, 3600)
141
+ minutes, secs = divmod(rem, 60)
142
+ if hours:
143
+ return f"{hours}h {minutes:02d}m {secs:02d}s"
144
+ if minutes:
145
+ return f"{minutes}m {secs:02d}s"
146
+ return f"{secs}s"
147
+
148
+
149
+ def format_burn_rate(rate: float | None) -> str:
150
+ if rate is None:
151
+ return "—"
152
+ sign = "+" if rate >= 0 else ""
153
+ return f"{sign}{rate:.2f}%/h"
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-meter
3
+ Version: 0.3.0
4
+ Summary: Local Codex usage intelligence for sessions, projects, models, cache, and rate-limit windows.
5
+ Project-URL: Homepage, https://github.com/rajdeepmondaldotcom/codex-meter
6
+ Project-URL: Repository, https://github.com/rajdeepmondaldotcom/codex-meter
7
+ Project-URL: Issues, https://github.com/rajdeepmondaldotcom/codex-meter/issues
8
+ Project-URL: Changelog, https://github.com/rajdeepmondaldotcom/codex-meter/blob/main/CHANGELOG.md
9
+ Author: Rajdeep Mondal
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,codex,credits,openai,tokens,usage
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: rich>=13.7
27
+ Requires-Dist: typer>=0.12
28
+ Provides-Extra: prom
29
+ Requires-Dist: prometheus-client>=0.20; extra == 'prom'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # codex-meter
33
+
34
+ [![CI](https://github.com/rajdeepmondaldotcom/codex-meter/actions/workflows/ci.yml/badge.svg)](https://github.com/rajdeepmondaldotcom/codex-meter/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/codex-meter.svg)](https://pypi.org/project/codex-meter/)
36
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
37
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
38
+
39
+ Understand your Codex work locally.
40
+
41
+ `codex-meter` turns local Codex logs into a clear record of how your coding work
42
+ happened: sessions, projects, models, tiers, cache reuse, tokens, credits,
43
+ API-equivalent dollars, and rate-limit windows.
44
+
45
+ No cloud sync. No account login. No billing scrape. Just local evidence you can
46
+ inspect, export, and trust.
47
+
48
+ ## Install
49
+
50
+ Requires Python 3.11+.
51
+
52
+ ```bash
53
+ uvx codex-meter
54
+ ```
55
+
56
+ Persistent install:
57
+
58
+ ```bash
59
+ uv tool install codex-meter
60
+ codex-meter
61
+ ```
62
+
63
+ With `pipx`:
64
+
65
+ ```bash
66
+ pipx install codex-meter
67
+ codex-meter
68
+ ```
69
+
70
+ With `pip`:
71
+
72
+ ```bash
73
+ python -m pip install codex-meter
74
+ codex-meter
75
+ ```
76
+
77
+ Prometheus exporter:
78
+
79
+ ```bash
80
+ uv tool install 'codex-meter[prom]'
81
+ ```
82
+
83
+ Homebrew:
84
+
85
+ ```bash
86
+ brew install rajdeepmondaldotcom/tap/codex-meter
87
+ ```
88
+
89
+ ## Start Here
90
+
91
+ ```bash
92
+ codex-meter # 7 / 30 / 90 day overview
93
+ codex-meter doctor # check local data and assumptions
94
+ codex-meter live # watch usage while you work
95
+ codex-meter project --days 30 # see project activity
96
+ codex-meter models --days 30 # inspect model and tier mix
97
+ ```
98
+
99
+ The first run parses local session files. Later runs use a sidecar parse cache.
100
+ Use `--no-parse-cache` when debugging parser behavior.
101
+
102
+ ## Why It Exists
103
+
104
+ Codex already leaves a detailed trail on your machine. The problem is that the
105
+ trail is split across JSONL sessions, SQLite metadata, config, rate-limit
106
+ samples, and pricing assumptions. `codex-meter` turns that trail into one local
107
+ view.
108
+
109
+ It answers:
110
+
111
+ - What did I use?
112
+ - Where did it go?
113
+ - Which model and tier drove it?
114
+ - How much input was cached?
115
+ - What changed across sessions, projects, and days?
116
+
117
+ This is not an OpenAI billing ledger. It is local usage intelligence.
118
+
119
+ ## Commands
120
+
121
+ | Need | Command |
122
+ | --- | --- |
123
+ | Overview | `codex-meter` |
124
+ | Daily / weekly / monthly | `codex-meter daily`, `weekly`, `monthly` |
125
+ | Sessions | `codex-meter session --top 20` |
126
+ | Projects | `codex-meter project --days 30` |
127
+ | Models and tiers | `codex-meter models --days 30` |
128
+ | Recent events | `codex-meter tail --n 20` |
129
+ | Rate limits | `codex-meter limits` |
130
+ | Insights | `codex-meter insights` |
131
+ | Forecast | `codex-meter forecast --days 14` |
132
+ | Compare windows | `codex-meter compare --a "last 7 days" --b "previous 7 days"` |
133
+ | What-if pricing | `codex-meter whatif --tier standard` |
134
+ | Optional budgets | `codex-meter budgets check` |
135
+ | Receipt | `codex-meter export receipt --month 2026-05 --format html` |
136
+ | Prometheus | `codex-meter export prometheus --port 9090` |
137
+ | Grafana | `codex-meter export grafana > dashboard.json` |
138
+
139
+ Most report commands support:
140
+
141
+ ```bash
142
+ --format table|json|csv|markdown
143
+ --output report.json
144
+ --since 2026-05-01
145
+ --days 30
146
+ --top 20
147
+ ```
148
+
149
+ ## Live View
150
+
151
+ ```bash
152
+ codex-meter live
153
+ ```
154
+
155
+ Shows today's usage, trailing 7-day usage, 5-hour and weekly windows, burn rate,
156
+ and ETA to 100% when enough samples exist.
157
+
158
+ Hotkeys:
159
+
160
+ | Key | Action |
161
+ | --- | --- |
162
+ | `q` | Quit |
163
+ | `?` | Help |
164
+ | `r` | Refresh |
165
+ | `p` | Pause |
166
+
167
+ ## Budgets
168
+
169
+ Create a config:
170
+
171
+ ```bash
172
+ codex-meter init
173
+ ```
174
+
175
+ Add limits:
176
+
177
+ ```toml
178
+ [budgets]
179
+ daily_credits = 25000
180
+ weekly_credits = 100000
181
+ monthly_credits = 400000
182
+ weekly_api_dollars = 50.0
183
+ ```
184
+
185
+ Check them:
186
+
187
+ ```bash
188
+ codex-meter budgets check
189
+ ```
190
+
191
+ Exit codes are built for automation:
192
+
193
+ | Exit | Meaning |
194
+ | ---: | --- |
195
+ | `0` | ok |
196
+ | `1` | warning |
197
+ | `2` | breached or failed |
198
+
199
+ ## Exports
200
+
201
+ ```bash
202
+ codex-meter export receipt --month 2026-05 --format markdown
203
+ codex-meter export receipt --month 2026-05 --format html > receipt.html
204
+ codex-meter export prometheus --host 127.0.0.1 --port 9090
205
+ codex-meter export grafana > dashboard.json
206
+ ```
207
+
208
+ Receipts redact local session and project labels by default. Use
209
+ `--show-sensitive` only for private artifacts.
210
+
211
+ Prometheus exposes credits, burn rate, rate-limit window percent, event count,
212
+ long-context event count, and tokens by model/tier/kind.
213
+
214
+ ## Data Sources
215
+
216
+ `codex-meter` reads:
217
+
218
+ - `~/.codex/sessions/**/*.jsonl`
219
+ - `~/.codex/state_5.sqlite`
220
+ - `~/.codex/config.toml`
221
+
222
+ Normal reports do not touch the network. The only networked command is explicit:
223
+
224
+ ```bash
225
+ codex-meter rates refresh --allow-network
226
+ ```
227
+
228
+ That writes a local pricing-source audit snapshot, including observed token
229
+ rates, fast multipliers, long-context rules, and discrepancies. It does not
230
+ rewrite the embedded rate card.
231
+
232
+ ## Accuracy
233
+
234
+ The hard part is not adding tokens. It is making assumptions visible.
235
+
236
+ `codex-meter` tracks cached input separately, applies per-model long-context
237
+ rules, uses exact decimal math internally, and reports whether service tiers
238
+ came from logs, config, overrides, or assumptions. If a model or credit rate is
239
+ not source-verified, reports mark pricing as partial instead of silently using a
240
+ fallback as exact.
241
+
242
+ Reasoning tokens are only billed separately when the Codex token log shows they
243
+ are not already included in output tokens. This prevents double-counting on
244
+ current local Codex logs.
245
+
246
+ Service-tier precedence:
247
+
248
+ 1. `--service-tier standard|fast`
249
+ 2. `--tier-overrides overrides.json`
250
+ 3. logged tier
251
+ 4. current `~/.codex/config.toml`
252
+ 5. `--unknown-service-tier`
253
+
254
+ Inspect pricing:
255
+
256
+ ```bash
257
+ codex-meter rates show
258
+ codex-meter rates show --format json
259
+ ```
260
+
261
+ Use local overrides when needed:
262
+
263
+ ```bash
264
+ codex-meter daily --rates-file ./rates.json
265
+ ```
266
+
267
+ Schemas:
268
+
269
+ - [`schemas/rates.schema.json`](schemas/rates.schema.json)
270
+ - [`schemas/tier-overrides.schema.json`](schemas/tier-overrides.schema.json)
271
+
272
+ ## Privacy
273
+
274
+ The default posture is local and conservative.
275
+
276
+ - Reports read local files.
277
+ - Prompt and session labels are redacted unless requested.
278
+ - Receipts hide full session IDs and project paths by default.
279
+ - JSON, CSV, Markdown, and HTML exports may still contain local metadata.
280
+
281
+ Treat exports as sensitive unless you made them for sharing.
282
+
283
+ ## Development
284
+
285
+ ```bash
286
+ uv sync --dev
287
+ uv run ruff check .
288
+ uv run ruff format --check .
289
+ PYTHONWARNINGS=error::ResourceWarning uv run pytest
290
+ uv run pytest --cov=src/codex_meter --cov-report=term
291
+ uv run python -m build
292
+ ```
293
+
294
+ Smoke test against your own logs:
295
+
296
+ ```bash
297
+ uv run codex-meter doctor
298
+ uv run codex-meter overview
299
+ uv run codex-meter rates show
300
+ ```
301
+
302
+ ## License
303
+
304
+ MIT
@@ -0,0 +1,26 @@
1
+ codex_meter/__init__.py,sha256=IwovSXgnigRH64qIbBoBdQHsLUqa43qhK2tSTsmLUuA,292
2
+ codex_meter/__main__.py,sha256=Il5u5iA1jLBTRWK1Moy2K30VdDdN5Lld6zk2DDpQqUE,70
3
+ codex_meter/aggregation.py,sha256=ZyM0t2vz8Ceu1Jadmt6aJI30N4ju_j1QrmuQbN7eVBk,4330
4
+ codex_meter/budgets.py,sha256=OBBIEznlb5skrSyB494zhMw8fj_rAaSIp8Xfuk11t58,4278
5
+ codex_meter/cli.py,sha256=r9kAweIrKTsyKa_nTQqVmMNsLx1QYCLHYPA0YsrSqRk,87046
6
+ codex_meter/config.py,sha256=E-KcV9eXafzAMHT8Rnu4e4zeE-aPwHmEcvOPj9HRorY,6164
7
+ codex_meter/exporters.py,sha256=holZtgJPGnlNNYOT7egqUbdbYwVr8Ezoo1K9cgNho7A,12420
8
+ codex_meter/forecasts.py,sha256=XflsG2e2JI-8eZWeZbZ3-i-oP955sg-sKHEkB0OAiJQ,2686
9
+ codex_meter/humanize.py,sha256=h4KPo0zdugB7EjVml6_ooOyCyEg7bE3j9mG9MSfYFMs,1177
10
+ codex_meter/insights.py,sha256=8fkzO9GyrcQp2JMakb7Drxew-F9leNWAvgfPuA0gy-4,4768
11
+ codex_meter/intervals.py,sha256=GWtUkPftEEwUmRwTJzGIEGVLZjvkhZC8gf0qS52fqkM,4590
12
+ codex_meter/live.py,sha256=shtXCRGm4fn4GwinDMXKv3-g77qyHocgg4291f3VBao,10184
13
+ codex_meter/models.py,sha256=8Idl9gN0keOSho2xZdKoz6yFoi2fBNyjZKp3Qp2RxjI,8238
14
+ codex_meter/parse_cache.py,sha256=gwHC0rqZic3KGmfc2g-I7lKLg387YFeS-gbvhE6I6ws,6143
15
+ codex_meter/parser.py,sha256=70hLQwNBviKJcETKe2lpGU6AIIS35MWiu898zzaYvS4,18645
16
+ codex_meter/pricing.py,sha256=OxcA3ETBZ5caHbHN2XC7MEzseuJG_3TgxZQbbm1o1YU,11547
17
+ codex_meter/prom_export.py,sha256=jPFPplmr9AICp805wv4YhlcgiSQ4Nrp5z_4soIU0sz0,3920
18
+ codex_meter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ codex_meter/render.py,sha256=67IyiXH-ciY347M9qhvGu6hcGWd5LoM93oYIQTgpiDY,19685
20
+ codex_meter/timeutil.py,sha256=0Vth-CWRwCmBh0oiGA_LkP9fsjk-Nts6SaNJI2LqiR0,2266
21
+ codex_meter/windows.py,sha256=Z2Ryst68Zu0usj3S_uvVuy_JiUFeB9vARwuryKeJq8k,4802
22
+ codex_meter-0.3.0.dist-info/METADATA,sha256=052G0GDMGwQPCJozZfcWMPResE8YZB9MgnrYu363MnQ,7934
23
+ codex_meter-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
24
+ codex_meter-0.3.0.dist-info/entry_points.txt,sha256=3i4pFl39F6QW_OJQ2Ds5wM40eqArMNkFeuLftAdB_wM,52
25
+ codex_meter-0.3.0.dist-info/licenses/LICENSE,sha256=o5_prA7Ia_-1E726qg4ApoZOsGLloxBTq-Z6CCXMc9Y,1071
26
+ codex_meter-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codex-meter = codex_meter.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rajdeep Mondal
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.