codex-usage-tracking 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_usage_tracker/__init__.py +7 -0
- codex_usage_tracker/__main__.py +6 -0
- codex_usage_tracker/allowance.py +759 -0
- codex_usage_tracker/api_payloads.py +90 -0
- codex_usage_tracker/cli.py +1326 -0
- codex_usage_tracker/context.py +410 -0
- codex_usage_tracker/costing.py +176 -0
- codex_usage_tracker/dashboard.py +389 -0
- codex_usage_tracker/diagnostics.py +624 -0
- codex_usage_tracker/formatting.py +225 -0
- codex_usage_tracker/json_contracts.py +350 -0
- codex_usage_tracker/mcp_server.py +371 -0
- codex_usage_tracker/models.py +92 -0
- codex_usage_tracker/parser.py +491 -0
- codex_usage_tracker/paths.py +18 -0
- codex_usage_tracker/plugin_data/__init__.py +1 -0
- codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
- codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
- codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
- codex_usage_tracker/plugin_installer.py +312 -0
- codex_usage_tracker/pricing.py +57 -0
- codex_usage_tracker/pricing_config.py +223 -0
- codex_usage_tracker/pricing_estimates.py +44 -0
- codex_usage_tracker/pricing_openai.py +253 -0
- codex_usage_tracker/projects.py +347 -0
- codex_usage_tracker/recommendations.py +270 -0
- codex_usage_tracker/reports.py +637 -0
- codex_usage_tracker/schema.py +71 -0
- codex_usage_tracker/server.py +400 -0
- codex_usage_tracker/store.py +666 -0
- codex_usage_tracker/support.py +147 -0
- codex_usage_tracker/threads.py +183 -0
- codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
- codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
- codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
- codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
- codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
- codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"""Codex usage allowance and credit estimation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from codex_usage_tracker.paths import DEFAULT_ALLOWANCE_PATH, DEFAULT_RATE_CARD_PATH
|
|
15
|
+
|
|
16
|
+
ALLOWANCE_SCHEMA = "codex-usage-tracker-allowance-v1"
|
|
17
|
+
RATE_CARD_SCHEMA = "codex-usage-tracker-codex-rate-card-v1"
|
|
18
|
+
CODEX_RATE_CARD_URL = "https://help.openai.com/en/articles/20001106-codex-rate-card"
|
|
19
|
+
CODEX_PRICING_URL = "https://developers.openai.com/codex/pricing"
|
|
20
|
+
DEFAULT_SOURCE = {
|
|
21
|
+
"name": "OpenAI Codex rate card",
|
|
22
|
+
"url": CODEX_RATE_CARD_URL,
|
|
23
|
+
"pricing_url": CODEX_PRICING_URL,
|
|
24
|
+
"fetched_at": "2026-06-03",
|
|
25
|
+
"basis": "credits per 1M input, cached input, and output tokens",
|
|
26
|
+
"tier": "standard",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ALLOWANCE_TEMPLATE = {
|
|
30
|
+
"schema": ALLOWANCE_SCHEMA,
|
|
31
|
+
"_comment": (
|
|
32
|
+
"Optional. Copy remaining usage values from Codex Settings > Usage or "
|
|
33
|
+
"from /status. Percent values can be 0-100 or 0-1. Add total_credits "
|
|
34
|
+
"only when your plan or workspace exposes an exact credit allowance. "
|
|
35
|
+
"Use credit_rates and aliases only for local rate-card overrides; "
|
|
36
|
+
"bundled/default rates live in the separate rate-card snapshot."
|
|
37
|
+
),
|
|
38
|
+
"windows": [
|
|
39
|
+
{
|
|
40
|
+
"key": "five_hour",
|
|
41
|
+
"label": "5h",
|
|
42
|
+
"remaining_percent": None,
|
|
43
|
+
"reset_at": None,
|
|
44
|
+
"captured_at": None,
|
|
45
|
+
"total_credits": None,
|
|
46
|
+
"remaining_credits": None,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"key": "weekly",
|
|
50
|
+
"label": "Weekly",
|
|
51
|
+
"remaining_percent": None,
|
|
52
|
+
"reset_at": None,
|
|
53
|
+
"captured_at": None,
|
|
54
|
+
"total_credits": None,
|
|
55
|
+
"remaining_credits": None,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
"credit_rates": {},
|
|
59
|
+
"aliases": {},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class AllowanceWindow:
|
|
65
|
+
"""One configured usage-limit window from the user's local allowance file."""
|
|
66
|
+
|
|
67
|
+
key: str
|
|
68
|
+
label: str
|
|
69
|
+
total_credits: float | None = None
|
|
70
|
+
remaining_credits: float | None = None
|
|
71
|
+
remaining_percent: float | None = None
|
|
72
|
+
reset_at: str | None = None
|
|
73
|
+
captured_at: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class UsageAllowanceConfig:
|
|
78
|
+
"""Local usage allowance config plus bundled Codex credit rates."""
|
|
79
|
+
|
|
80
|
+
path: Path
|
|
81
|
+
rate_card_path: Path
|
|
82
|
+
credit_rates: dict[str, dict[str, float]]
|
|
83
|
+
aliases: dict[str, dict[str, str]]
|
|
84
|
+
rate_metadata: dict[str, dict[str, Any]]
|
|
85
|
+
alias_metadata: dict[str, dict[str, Any]]
|
|
86
|
+
windows: list[AllowanceWindow]
|
|
87
|
+
loaded: bool
|
|
88
|
+
rate_card_loaded: bool
|
|
89
|
+
source: dict[str, Any]
|
|
90
|
+
error: str | None = None
|
|
91
|
+
rate_card_error: str | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class RateCardUpdateResult:
|
|
96
|
+
"""Result from writing a local Codex credit rate-card snapshot."""
|
|
97
|
+
|
|
98
|
+
path: Path
|
|
99
|
+
source_url: str | None
|
|
100
|
+
fetched_at: str | None
|
|
101
|
+
model_count: int
|
|
102
|
+
alias_count: int
|
|
103
|
+
backup_path: Path | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_allowance_config(
|
|
107
|
+
path: Path = DEFAULT_ALLOWANCE_PATH,
|
|
108
|
+
*,
|
|
109
|
+
rate_card_path: Path = DEFAULT_RATE_CARD_PATH,
|
|
110
|
+
) -> UsageAllowanceConfig:
|
|
111
|
+
"""Load optional allowance settings while always keeping bundled rate-card data."""
|
|
112
|
+
|
|
113
|
+
base_card = load_bundled_rate_card()
|
|
114
|
+
rate_card_loaded = False
|
|
115
|
+
rate_card_error = None
|
|
116
|
+
if rate_card_path.expanduser().exists():
|
|
117
|
+
try:
|
|
118
|
+
base_card = _load_json_file(rate_card_path)
|
|
119
|
+
rate_card_loaded = True
|
|
120
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
121
|
+
rate_card_error = str(exc)
|
|
122
|
+
|
|
123
|
+
source = parse_rate_card_source(base_card)
|
|
124
|
+
credit_rates = parse_credit_rates(base_card.get("credit_rates", {}))
|
|
125
|
+
aliases = parse_aliases(base_card.get("aliases", {}))
|
|
126
|
+
rate_metadata = parse_credit_rate_metadata(
|
|
127
|
+
base_card.get("credit_rates", {}), source=source, default_confidence="exact"
|
|
128
|
+
)
|
|
129
|
+
alias_metadata = parse_alias_metadata(base_card.get("aliases", {}), source=source)
|
|
130
|
+
windows: list[AllowanceWindow] = []
|
|
131
|
+
if not path.exists():
|
|
132
|
+
return UsageAllowanceConfig(
|
|
133
|
+
path=path,
|
|
134
|
+
rate_card_path=rate_card_path,
|
|
135
|
+
credit_rates=credit_rates,
|
|
136
|
+
aliases=aliases,
|
|
137
|
+
rate_metadata=rate_metadata,
|
|
138
|
+
alias_metadata=alias_metadata,
|
|
139
|
+
windows=windows,
|
|
140
|
+
loaded=False,
|
|
141
|
+
rate_card_loaded=rate_card_loaded,
|
|
142
|
+
source=source,
|
|
143
|
+
rate_card_error=rate_card_error,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
raw = _load_json_file(path)
|
|
148
|
+
local_rates = parse_credit_rates(raw.get("credit_rates", {}))
|
|
149
|
+
credit_rates.update(local_rates)
|
|
150
|
+
rate_metadata.update(
|
|
151
|
+
parse_credit_rate_metadata(
|
|
152
|
+
raw.get("credit_rates", {}),
|
|
153
|
+
source={
|
|
154
|
+
"name": "Local allowance override",
|
|
155
|
+
"url": str(path.expanduser()),
|
|
156
|
+
"fetched_at": None,
|
|
157
|
+
},
|
|
158
|
+
default_confidence="user_override",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
local_aliases = parse_aliases(raw.get("aliases", {}))
|
|
162
|
+
aliases.update(local_aliases)
|
|
163
|
+
alias_metadata.update(
|
|
164
|
+
parse_alias_metadata(
|
|
165
|
+
raw.get("aliases", {}),
|
|
166
|
+
source={
|
|
167
|
+
"name": "Local allowance override",
|
|
168
|
+
"url": str(path.expanduser()),
|
|
169
|
+
"fetched_at": None,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
windows = parse_windows(raw.get("windows", []))
|
|
174
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
175
|
+
return UsageAllowanceConfig(
|
|
176
|
+
path=path,
|
|
177
|
+
rate_card_path=rate_card_path,
|
|
178
|
+
credit_rates=credit_rates,
|
|
179
|
+
aliases=aliases,
|
|
180
|
+
rate_metadata=rate_metadata,
|
|
181
|
+
alias_metadata=alias_metadata,
|
|
182
|
+
windows=[],
|
|
183
|
+
loaded=False,
|
|
184
|
+
rate_card_loaded=rate_card_loaded,
|
|
185
|
+
source=source,
|
|
186
|
+
error=str(exc),
|
|
187
|
+
rate_card_error=rate_card_error,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return UsageAllowanceConfig(
|
|
191
|
+
path=path,
|
|
192
|
+
rate_card_path=rate_card_path,
|
|
193
|
+
credit_rates=credit_rates,
|
|
194
|
+
aliases=aliases,
|
|
195
|
+
rate_metadata=rate_metadata,
|
|
196
|
+
alias_metadata=alias_metadata,
|
|
197
|
+
windows=windows,
|
|
198
|
+
loaded=True,
|
|
199
|
+
rate_card_loaded=rate_card_loaded,
|
|
200
|
+
source=source,
|
|
201
|
+
rate_card_error=rate_card_error,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def write_allowance_template(
|
|
206
|
+
path: Path = DEFAULT_ALLOWANCE_PATH, force: bool = False
|
|
207
|
+
) -> Path:
|
|
208
|
+
"""Write a local template for optional allowance-window settings."""
|
|
209
|
+
|
|
210
|
+
if path.exists() and not force:
|
|
211
|
+
raise FileExistsError(f"Allowance config already exists: {path}")
|
|
212
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
path.write_text(json.dumps(ALLOWANCE_TEMPLATE, indent=2) + "\n", encoding="utf-8")
|
|
214
|
+
return path
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def load_bundled_rate_card() -> dict[str, Any]:
|
|
218
|
+
"""Load the package-bundled Codex credit rate-card snapshot."""
|
|
219
|
+
|
|
220
|
+
rate_card = (
|
|
221
|
+
resources.files("codex_usage_tracker.plugin_data")
|
|
222
|
+
.joinpath("rate_cards")
|
|
223
|
+
.joinpath("codex-credit-rates.json")
|
|
224
|
+
)
|
|
225
|
+
with rate_card.open("r", encoding="utf-8") as handle:
|
|
226
|
+
raw = json.load(handle)
|
|
227
|
+
if not isinstance(raw, dict):
|
|
228
|
+
raise ValueError("bundled Codex rate card must be a JSON object")
|
|
229
|
+
return raw
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def update_rate_card(
|
|
233
|
+
path: Path = DEFAULT_RATE_CARD_PATH,
|
|
234
|
+
*,
|
|
235
|
+
source_file: Path | None = None,
|
|
236
|
+
) -> RateCardUpdateResult:
|
|
237
|
+
"""Write a validated Codex credit rate-card snapshot to the local config directory."""
|
|
238
|
+
|
|
239
|
+
raw = _load_json_file(source_file) if source_file is not None else load_bundled_rate_card()
|
|
240
|
+
schema = raw.get("schema") or raw.get("_schema")
|
|
241
|
+
if schema and schema != RATE_CARD_SCHEMA:
|
|
242
|
+
raise ValueError(f"unsupported Codex rate-card schema: {schema}")
|
|
243
|
+
source = parse_rate_card_source(raw)
|
|
244
|
+
credit_rates = parse_credit_rates(raw.get("credit_rates", {}))
|
|
245
|
+
aliases = parse_aliases(raw.get("aliases", {}))
|
|
246
|
+
if not credit_rates:
|
|
247
|
+
raise ValueError("rate card must contain at least one credit rate")
|
|
248
|
+
parse_credit_rate_metadata(raw.get("credit_rates", {}), source=source)
|
|
249
|
+
parse_alias_metadata(raw.get("aliases", {}), source=source)
|
|
250
|
+
|
|
251
|
+
path = path.expanduser()
|
|
252
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
backup_path = _backup_existing_rate_card(path)
|
|
254
|
+
tmp_path = path.with_name(f"{path.name}.tmp")
|
|
255
|
+
tmp_path.write_text(json.dumps(raw, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
256
|
+
tmp_path.replace(path)
|
|
257
|
+
return RateCardUpdateResult(
|
|
258
|
+
path=path,
|
|
259
|
+
source_url=_optional_str(source.get("url")),
|
|
260
|
+
fetched_at=_optional_str(source.get("fetched_at")),
|
|
261
|
+
model_count=len(credit_rates),
|
|
262
|
+
alias_count=len(aliases),
|
|
263
|
+
backup_path=backup_path,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_allowance_text(
|
|
268
|
+
text: str,
|
|
269
|
+
*,
|
|
270
|
+
captured_at: str | None = None,
|
|
271
|
+
) -> list[AllowanceWindow]:
|
|
272
|
+
"""Parse pasted Codex usage text into allowance windows."""
|
|
273
|
+
|
|
274
|
+
captured = captured_at or _utc_now()
|
|
275
|
+
windows: list[AllowanceWindow] = []
|
|
276
|
+
for key, label, percent, reset_at in _allowance_line_matches(text):
|
|
277
|
+
windows.append(
|
|
278
|
+
AllowanceWindow(
|
|
279
|
+
key=key,
|
|
280
|
+
label=label,
|
|
281
|
+
remaining_percent=_optional_percent(percent),
|
|
282
|
+
reset_at=reset_at,
|
|
283
|
+
captured_at=captured,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
return windows
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def write_allowance_from_text(
|
|
290
|
+
text: str,
|
|
291
|
+
*,
|
|
292
|
+
path: Path = DEFAULT_ALLOWANCE_PATH,
|
|
293
|
+
force: bool = False,
|
|
294
|
+
captured_at: str | None = None,
|
|
295
|
+
) -> Path:
|
|
296
|
+
"""Update the local allowance-window file from pasted usage text."""
|
|
297
|
+
|
|
298
|
+
windows = parse_allowance_text(text, captured_at=captured_at)
|
|
299
|
+
if not windows:
|
|
300
|
+
raise ValueError("could not find 5h or weekly allowance percentages in pasted text")
|
|
301
|
+
|
|
302
|
+
path = path.expanduser()
|
|
303
|
+
if path.exists():
|
|
304
|
+
try:
|
|
305
|
+
payload = _load_json_file(path)
|
|
306
|
+
except (OSError, TypeError, json.JSONDecodeError, ValueError):
|
|
307
|
+
if not force:
|
|
308
|
+
raise
|
|
309
|
+
payload = json.loads(json.dumps(ALLOWANCE_TEMPLATE))
|
|
310
|
+
else:
|
|
311
|
+
payload = json.loads(json.dumps(ALLOWANCE_TEMPLATE))
|
|
312
|
+
payload["schema"] = ALLOWANCE_SCHEMA
|
|
313
|
+
payload["windows"] = [asdict(window) for window in windows]
|
|
314
|
+
payload["_source"] = {
|
|
315
|
+
"name": "Pasted Codex usage text",
|
|
316
|
+
"captured_at": windows[0].captured_at,
|
|
317
|
+
"exact_allowance_source": False,
|
|
318
|
+
"note": "Remaining percentages are user-copied from Codex UI or /status text.",
|
|
319
|
+
}
|
|
320
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
322
|
+
return path
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def annotate_rows_with_allowance(
|
|
326
|
+
rows: list[dict[str, Any]],
|
|
327
|
+
config: UsageAllowanceConfig | None = None,
|
|
328
|
+
*,
|
|
329
|
+
model_field: str = "model",
|
|
330
|
+
allowance_path: Path = DEFAULT_ALLOWANCE_PATH,
|
|
331
|
+
) -> list[dict[str, Any]]:
|
|
332
|
+
"""Return copied rows with Codex credit usage annotations."""
|
|
333
|
+
|
|
334
|
+
resolved = config or load_allowance_config(allowance_path)
|
|
335
|
+
annotated: list[dict[str, Any]] = []
|
|
336
|
+
for row in rows:
|
|
337
|
+
copy = dict(row)
|
|
338
|
+
model = copy.get(model_field)
|
|
339
|
+
match = resolve_credit_rate(model, resolved)
|
|
340
|
+
if match is None:
|
|
341
|
+
copy.update(
|
|
342
|
+
{
|
|
343
|
+
"usage_credits": None,
|
|
344
|
+
"usage_credit_model": None,
|
|
345
|
+
"usage_credit_confidence": "unpriced",
|
|
346
|
+
"usage_credit_source": "No Codex credit rate",
|
|
347
|
+
"usage_credit_source_url": None,
|
|
348
|
+
"usage_credit_fetched_at": None,
|
|
349
|
+
"usage_credit_tier": None,
|
|
350
|
+
"usage_credit_note": "No bundled or configured credit rate matched this model.",
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
rated_model, rates, confidence, note, metadata = match
|
|
355
|
+
copy.update(
|
|
356
|
+
{
|
|
357
|
+
"usage_credits": estimate_usage_credits(copy, rates),
|
|
358
|
+
"usage_credit_model": rated_model,
|
|
359
|
+
"usage_credit_confidence": confidence,
|
|
360
|
+
"usage_credit_source": metadata.get("source_name")
|
|
361
|
+
or resolved.source.get("name", "Codex credit rates"),
|
|
362
|
+
"usage_credit_source_url": metadata.get("source_url"),
|
|
363
|
+
"usage_credit_fetched_at": metadata.get("fetched_at"),
|
|
364
|
+
"usage_credit_tier": metadata.get("tier"),
|
|
365
|
+
"usage_credit_note": note,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
annotated.append(copy)
|
|
369
|
+
return annotated
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def summarize_allowance_usage(
|
|
373
|
+
rows: list[dict[str, Any]], config: UsageAllowanceConfig | None = None
|
|
374
|
+
) -> dict[str, Any]:
|
|
375
|
+
"""Summarize Codex credit usage and configured allowance windows."""
|
|
376
|
+
|
|
377
|
+
resolved = config or load_allowance_config()
|
|
378
|
+
total_tokens = sum(_number(row.get("total_tokens")) for row in rows)
|
|
379
|
+
rated_tokens = sum(
|
|
380
|
+
_number(row.get("total_tokens"))
|
|
381
|
+
for row in rows
|
|
382
|
+
if row.get("usage_credits") is not None
|
|
383
|
+
)
|
|
384
|
+
usage_credits = sum(
|
|
385
|
+
_number(row.get("usage_credits"))
|
|
386
|
+
for row in rows
|
|
387
|
+
if row.get("usage_credits") is not None
|
|
388
|
+
)
|
|
389
|
+
estimated_credits = sum(
|
|
390
|
+
_number(row.get("usage_credits"))
|
|
391
|
+
for row in rows
|
|
392
|
+
if row.get("usage_credit_confidence") == "estimated"
|
|
393
|
+
)
|
|
394
|
+
override_credits = sum(
|
|
395
|
+
_number(row.get("usage_credits"))
|
|
396
|
+
for row in rows
|
|
397
|
+
if row.get("usage_credit_confidence") == "user_override"
|
|
398
|
+
)
|
|
399
|
+
exact_credits = sum(
|
|
400
|
+
_number(row.get("usage_credits"))
|
|
401
|
+
for row in rows
|
|
402
|
+
if row.get("usage_credit_confidence") == "exact"
|
|
403
|
+
)
|
|
404
|
+
return {
|
|
405
|
+
"usage_credits": usage_credits,
|
|
406
|
+
"exact_usage_credits": exact_credits,
|
|
407
|
+
"estimated_usage_credits": estimated_credits,
|
|
408
|
+
"user_override_usage_credits": override_credits,
|
|
409
|
+
"rated_tokens": rated_tokens,
|
|
410
|
+
"unrated_tokens": max(total_tokens - rated_tokens, 0.0),
|
|
411
|
+
"credit_token_ratio": rated_tokens / total_tokens if total_tokens else 0.0,
|
|
412
|
+
"windows": [asdict(window) for window in resolved.windows],
|
|
413
|
+
"source": resolved.source,
|
|
414
|
+
"configured": resolved.loaded,
|
|
415
|
+
"error": resolved.error,
|
|
416
|
+
"rate_card_loaded": resolved.rate_card_loaded,
|
|
417
|
+
"rate_card_error": resolved.rate_card_error,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def resolve_credit_rate(
|
|
422
|
+
model: object, config: UsageAllowanceConfig
|
|
423
|
+
) -> tuple[str, dict[str, float], str, str, dict[str, Any]] | None:
|
|
424
|
+
"""Resolve a model label into a credit rate, confidence, and note."""
|
|
425
|
+
|
|
426
|
+
normalized = _normalize_model(model)
|
|
427
|
+
if not normalized:
|
|
428
|
+
return None
|
|
429
|
+
direct = config.credit_rates.get(normalized)
|
|
430
|
+
if direct is not None:
|
|
431
|
+
metadata = config.rate_metadata.get(normalized, {})
|
|
432
|
+
confidence = _optional_str(metadata.get("confidence")) or "exact"
|
|
433
|
+
note = _optional_str(metadata.get("note")) or (
|
|
434
|
+
"Direct match to Codex credit rates."
|
|
435
|
+
if confidence != "user_override"
|
|
436
|
+
else "Direct match to local user-provided Codex credit rate."
|
|
437
|
+
)
|
|
438
|
+
return normalized, direct, confidence, note, metadata
|
|
439
|
+
|
|
440
|
+
alias = config.aliases.get(normalized)
|
|
441
|
+
if not alias:
|
|
442
|
+
return None
|
|
443
|
+
target = _normalize_model(alias.get("model"))
|
|
444
|
+
if not target:
|
|
445
|
+
return None
|
|
446
|
+
rates = config.credit_rates.get(target)
|
|
447
|
+
if rates is None:
|
|
448
|
+
return None
|
|
449
|
+
metadata = {**config.rate_metadata.get(target, {}), **config.alias_metadata.get(normalized, {})}
|
|
450
|
+
confidence = alias.get("confidence") or _optional_str(metadata.get("confidence")) or "estimated"
|
|
451
|
+
note = alias.get("note") or _optional_str(metadata.get("note")) or (
|
|
452
|
+
f"Mapped from {normalized} to {target} by local alias."
|
|
453
|
+
)
|
|
454
|
+
return target, rates, confidence, note, metadata
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def estimate_usage_credits(row: dict[str, Any], rates: dict[str, float]) -> float:
|
|
458
|
+
"""Estimate Codex credits from aggregate token counters."""
|
|
459
|
+
|
|
460
|
+
input_rate = rates["input_per_million"]
|
|
461
|
+
cached_rate = rates["cached_input_per_million"]
|
|
462
|
+
output_rate = rates["output_per_million"]
|
|
463
|
+
cached_input = _number(row.get("cached_input_tokens"))
|
|
464
|
+
uncached_input = _number(row.get("uncached_input_tokens"))
|
|
465
|
+
if uncached_input <= 0:
|
|
466
|
+
uncached_input = max(_number(row.get("input_tokens")) - cached_input, 0.0)
|
|
467
|
+
output_tokens = _number(row.get("output_tokens"))
|
|
468
|
+
return (
|
|
469
|
+
(uncached_input * input_rate)
|
|
470
|
+
+ (cached_input * cached_rate)
|
|
471
|
+
+ (output_tokens * output_rate)
|
|
472
|
+
) / 1_000_000
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def parse_credit_rates(raw: object) -> dict[str, dict[str, float]]:
|
|
476
|
+
if not isinstance(raw, dict):
|
|
477
|
+
return {}
|
|
478
|
+
parsed: dict[str, dict[str, float]] = {}
|
|
479
|
+
for model, rates in raw.items():
|
|
480
|
+
normalized = _normalize_model(model)
|
|
481
|
+
if not normalized or not isinstance(rates, dict):
|
|
482
|
+
continue
|
|
483
|
+
parsed[normalized] = {
|
|
484
|
+
"input_per_million": _required_rate(rates, "input_per_million", normalized),
|
|
485
|
+
"cached_input_per_million": _required_rate(
|
|
486
|
+
rates, "cached_input_per_million", normalized
|
|
487
|
+
),
|
|
488
|
+
"output_per_million": _required_rate(rates, "output_per_million", normalized),
|
|
489
|
+
}
|
|
490
|
+
return parsed
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def parse_aliases(raw: object) -> dict[str, dict[str, str]]:
|
|
494
|
+
if not isinstance(raw, dict):
|
|
495
|
+
return {}
|
|
496
|
+
parsed: dict[str, dict[str, str]] = {}
|
|
497
|
+
for source, target in raw.items():
|
|
498
|
+
source_model = _normalize_model(source)
|
|
499
|
+
if not source_model:
|
|
500
|
+
continue
|
|
501
|
+
if isinstance(target, str):
|
|
502
|
+
parsed[source_model] = {
|
|
503
|
+
"model": _normalize_model(target) or target,
|
|
504
|
+
"confidence": "estimated",
|
|
505
|
+
"note": f"Mapped from {source_model} by local allowance config.",
|
|
506
|
+
}
|
|
507
|
+
elif isinstance(target, dict):
|
|
508
|
+
target_model = _normalize_model(target.get("model"))
|
|
509
|
+
if not target_model:
|
|
510
|
+
continue
|
|
511
|
+
parsed[source_model] = {
|
|
512
|
+
"model": target_model,
|
|
513
|
+
"confidence": _optional_str(target.get("confidence")) or "estimated",
|
|
514
|
+
"note": _optional_str(target.get("note"))
|
|
515
|
+
or f"Mapped from {source_model} by local allowance config.",
|
|
516
|
+
}
|
|
517
|
+
return parsed
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def parse_rate_card_source(raw: object) -> dict[str, Any]:
|
|
521
|
+
if not isinstance(raw, dict):
|
|
522
|
+
return dict(DEFAULT_SOURCE)
|
|
523
|
+
source = raw.get("source") or raw.get("_source")
|
|
524
|
+
if not isinstance(source, dict):
|
|
525
|
+
return dict(DEFAULT_SOURCE)
|
|
526
|
+
return {**DEFAULT_SOURCE, **source}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def parse_credit_rate_metadata(
|
|
530
|
+
raw: object,
|
|
531
|
+
*,
|
|
532
|
+
source: dict[str, Any],
|
|
533
|
+
default_confidence: str = "exact",
|
|
534
|
+
) -> dict[str, dict[str, Any]]:
|
|
535
|
+
if not isinstance(raw, dict):
|
|
536
|
+
return {}
|
|
537
|
+
parsed: dict[str, dict[str, Any]] = {}
|
|
538
|
+
for model, rates in raw.items():
|
|
539
|
+
normalized = _normalize_model(model)
|
|
540
|
+
if not normalized or not isinstance(rates, dict):
|
|
541
|
+
continue
|
|
542
|
+
parsed[normalized] = {
|
|
543
|
+
"confidence": _optional_str(rates.get("confidence")) or default_confidence,
|
|
544
|
+
"source_name": _optional_str(rates.get("source_name"))
|
|
545
|
+
or _optional_str(source.get("name"))
|
|
546
|
+
or "Codex credit rates",
|
|
547
|
+
"source_url": _optional_str(rates.get("source_url"))
|
|
548
|
+
or _optional_str(source.get("url")),
|
|
549
|
+
"fetched_at": _optional_str(rates.get("fetched_at"))
|
|
550
|
+
or _optional_str(source.get("fetched_at")),
|
|
551
|
+
"tier": _optional_str(rates.get("tier")) or _optional_str(source.get("tier")),
|
|
552
|
+
"note": _optional_str(rates.get("note")),
|
|
553
|
+
}
|
|
554
|
+
return parsed
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def parse_alias_metadata(raw: object, *, source: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
558
|
+
if not isinstance(raw, dict):
|
|
559
|
+
return {}
|
|
560
|
+
parsed: dict[str, dict[str, Any]] = {}
|
|
561
|
+
for alias, target in raw.items():
|
|
562
|
+
normalized = _normalize_model(alias)
|
|
563
|
+
if not normalized:
|
|
564
|
+
continue
|
|
565
|
+
if isinstance(target, str):
|
|
566
|
+
parsed[normalized] = {
|
|
567
|
+
"confidence": "estimated",
|
|
568
|
+
"source_name": _optional_str(source.get("name")) or "Codex credit rates",
|
|
569
|
+
"source_url": _optional_str(source.get("url")),
|
|
570
|
+
"fetched_at": _optional_str(source.get("fetched_at")),
|
|
571
|
+
"tier": _optional_str(source.get("tier")),
|
|
572
|
+
"note": f"Mapped from {normalized} by local allowance config.",
|
|
573
|
+
}
|
|
574
|
+
elif isinstance(target, dict):
|
|
575
|
+
parsed[normalized] = {
|
|
576
|
+
"confidence": _optional_str(target.get("confidence")) or "estimated",
|
|
577
|
+
"source_name": _optional_str(target.get("source_name"))
|
|
578
|
+
or _optional_str(source.get("name"))
|
|
579
|
+
or "Codex credit rates",
|
|
580
|
+
"source_url": _optional_str(target.get("source_url"))
|
|
581
|
+
or _optional_str(source.get("url")),
|
|
582
|
+
"fetched_at": _optional_str(target.get("fetched_at"))
|
|
583
|
+
or _optional_str(source.get("fetched_at")),
|
|
584
|
+
"tier": _optional_str(target.get("tier")) or _optional_str(source.get("tier")),
|
|
585
|
+
"note": _optional_str(target.get("note")),
|
|
586
|
+
"alias_reason": _optional_str(target.get("alias_reason")),
|
|
587
|
+
}
|
|
588
|
+
return parsed
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def parse_windows(raw: object) -> list[AllowanceWindow]:
|
|
592
|
+
if isinstance(raw, dict):
|
|
593
|
+
rows = [{**value, "key": key} for key, value in raw.items() if isinstance(value, dict)]
|
|
594
|
+
elif isinstance(raw, list):
|
|
595
|
+
rows = [value for value in raw if isinstance(value, dict)]
|
|
596
|
+
else:
|
|
597
|
+
rows = []
|
|
598
|
+
|
|
599
|
+
windows: list[AllowanceWindow] = []
|
|
600
|
+
for row in rows:
|
|
601
|
+
key = _optional_str(row.get("key"))
|
|
602
|
+
if not key:
|
|
603
|
+
continue
|
|
604
|
+
label = _optional_str(row.get("label")) or key.replace("_", " ").title()
|
|
605
|
+
windows.append(
|
|
606
|
+
AllowanceWindow(
|
|
607
|
+
key=key,
|
|
608
|
+
label=label,
|
|
609
|
+
total_credits=_optional_positive_number(row.get("total_credits")),
|
|
610
|
+
remaining_credits=_optional_positive_number(row.get("remaining_credits")),
|
|
611
|
+
remaining_percent=_optional_percent(row.get("remaining_percent")),
|
|
612
|
+
reset_at=_optional_str(row.get("reset_at")),
|
|
613
|
+
captured_at=_optional_str(row.get("captured_at")),
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
return windows
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _load_json_file(path: Path) -> dict[str, Any]:
|
|
620
|
+
raw = json.loads(path.expanduser().read_text(encoding="utf-8"))
|
|
621
|
+
if not isinstance(raw, dict):
|
|
622
|
+
raise ValueError(f"JSON config must be an object: {path}")
|
|
623
|
+
return raw
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _backup_existing_rate_card(path: Path) -> Path | None:
|
|
627
|
+
if not path.exists():
|
|
628
|
+
return None
|
|
629
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
630
|
+
backup_path = path.with_name(f"{path.name}.{stamp}.bak")
|
|
631
|
+
shutil.copy2(path, backup_path)
|
|
632
|
+
return backup_path
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _allowance_line_matches(text: str) -> list[tuple[str, str, str, str | None]]:
|
|
636
|
+
lines = [line.strip() for line in text.replace("\u00a0", " ").splitlines() if line.strip()]
|
|
637
|
+
matches: list[tuple[str, str, str, str | None]] = []
|
|
638
|
+
for line in lines:
|
|
639
|
+
match = _ALLOWANCE_LINE_RE.match(line)
|
|
640
|
+
if not match:
|
|
641
|
+
continue
|
|
642
|
+
key = _allowance_window_key(match.group("label"))
|
|
643
|
+
if key is None:
|
|
644
|
+
continue
|
|
645
|
+
reset_at = match.group("reset")
|
|
646
|
+
if reset_at and _ALLOWANCE_LABEL_RE.search(reset_at):
|
|
647
|
+
continue
|
|
648
|
+
matches.append(
|
|
649
|
+
(
|
|
650
|
+
key,
|
|
651
|
+
"5h" if key == "five_hour" else "Weekly",
|
|
652
|
+
match.group("percent"),
|
|
653
|
+
reset_at.strip() if reset_at and reset_at.strip() else None,
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
if matches:
|
|
657
|
+
return _dedupe_allowance_matches(matches)
|
|
658
|
+
|
|
659
|
+
flat = " ".join(text.replace("\u00a0", " ").split())
|
|
660
|
+
label_matches = list(_ALLOWANCE_LABEL_RE.finditer(flat))
|
|
661
|
+
for index, match in enumerate(label_matches):
|
|
662
|
+
key = _allowance_window_key(match.group(0))
|
|
663
|
+
if key is None:
|
|
664
|
+
continue
|
|
665
|
+
next_start = label_matches[index + 1].start() if index + 1 < len(label_matches) else len(flat)
|
|
666
|
+
segment = flat[match.end() : next_start].strip()
|
|
667
|
+
percent_match = _ALLOWANCE_PERCENT_RE.search(segment)
|
|
668
|
+
if percent_match is None:
|
|
669
|
+
continue
|
|
670
|
+
reset_at = segment[percent_match.end() :].strip()
|
|
671
|
+
matches.append(
|
|
672
|
+
(
|
|
673
|
+
key,
|
|
674
|
+
"5h" if key == "five_hour" else "Weekly",
|
|
675
|
+
percent_match.group("percent"),
|
|
676
|
+
reset_at or None,
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
return _dedupe_allowance_matches(matches)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _dedupe_allowance_matches(
|
|
683
|
+
matches: list[tuple[str, str, str, str | None]],
|
|
684
|
+
) -> list[tuple[str, str, str, str | None]]:
|
|
685
|
+
seen: set[str] = set()
|
|
686
|
+
deduped: list[tuple[str, str, str, str | None]] = []
|
|
687
|
+
for match in matches:
|
|
688
|
+
key = match[0]
|
|
689
|
+
if key in seen:
|
|
690
|
+
continue
|
|
691
|
+
seen.add(key)
|
|
692
|
+
deduped.append(match)
|
|
693
|
+
return deduped
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _allowance_window_key(label: str) -> str | None:
|
|
697
|
+
normalized = label.lower().replace("-", "_").replace(" ", "_")
|
|
698
|
+
if normalized in {"5h", "5_hour", "five_hour"}:
|
|
699
|
+
return "five_hour"
|
|
700
|
+
if normalized in {"weekly", "week"}:
|
|
701
|
+
return "weekly"
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _normalize_model(value: object) -> str | None:
|
|
706
|
+
if not isinstance(value, str) or not value.strip():
|
|
707
|
+
return None
|
|
708
|
+
return value.strip().lower().replace("_", "-")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _required_rate(raw: dict[str, Any], key: str, model: str) -> float:
|
|
712
|
+
parsed = _optional_positive_number(raw.get(key))
|
|
713
|
+
if parsed is None:
|
|
714
|
+
raise ValueError(f"missing {key} for Codex credit model {model}")
|
|
715
|
+
return parsed
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _optional_positive_number(value: object) -> float | None:
|
|
719
|
+
if value is None or value == "":
|
|
720
|
+
return None
|
|
721
|
+
number = _number(value)
|
|
722
|
+
if number < 0:
|
|
723
|
+
raise ValueError("allowance values cannot be negative")
|
|
724
|
+
return number
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _optional_percent(value: object) -> float | None:
|
|
728
|
+
parsed = _optional_positive_number(value)
|
|
729
|
+
if parsed is None:
|
|
730
|
+
return None
|
|
731
|
+
return parsed / 100 if parsed > 1 else parsed
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _optional_str(value: object) -> str | None:
|
|
735
|
+
return value if isinstance(value, str) and value.strip() else None
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _number(value: object) -> float:
|
|
739
|
+
if isinstance(value, bool):
|
|
740
|
+
return float(int(value))
|
|
741
|
+
if isinstance(value, int | float):
|
|
742
|
+
return float(value)
|
|
743
|
+
if isinstance(value, str) and value.strip():
|
|
744
|
+
return float(value)
|
|
745
|
+
return 0.0
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _utc_now() -> str:
|
|
749
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
_ALLOWANCE_LINE_RE = re.compile(
|
|
753
|
+
r"^(?P<label>5h|5-hour|five-hour|weekly|week)\s+"
|
|
754
|
+
r"(?P<percent>\d+(?:\.\d+)?)\s*%"
|
|
755
|
+
r"(?:\s+(?P<reset>.+?))?\s*$",
|
|
756
|
+
re.IGNORECASE,
|
|
757
|
+
)
|
|
758
|
+
_ALLOWANCE_LABEL_RE = re.compile(r"\b(?:5h|5-hour|five-hour|weekly|week)\b", re.IGNORECASE)
|
|
759
|
+
_ALLOWANCE_PERCENT_RE = re.compile(r"(?P<percent>\d+(?:\.\d+)?)\s*%")
|