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,410 @@
|
|
|
1
|
+
"""Lazy raw-context loading for one aggregate usage record."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from codex_usage_tracker.paths import DEFAULT_DB_PATH
|
|
11
|
+
from codex_usage_tracker.store import query_usage_record
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONTEXT_CHARS = 20_000
|
|
14
|
+
DEFAULT_CONTEXT_ENTRIES = 80
|
|
15
|
+
|
|
16
|
+
_SECRET_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
17
|
+
(re.compile(r"sk-[A-Za-z0-9_-]{10,}"), "[REDACTED_OPENAI_KEY]"),
|
|
18
|
+
(re.compile(r"github" r"_pat_[A-Za-z0-9_]{20,}"), "[REDACTED_GITHUB_TOKEN]"),
|
|
19
|
+
(re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"), "[REDACTED_GITHUB_TOKEN]"),
|
|
20
|
+
(re.compile(r"\bA(?:KI|SI)A[0-9A-Z]{16}\b"), "[REDACTED_AWS_ACCESS_KEY]"),
|
|
21
|
+
(
|
|
22
|
+
re.compile(r"(?i)\baws_secret_access_key\s*[:=]\s*(['\"]?)[A-Za-z0-9/+=]{30,}\1"),
|
|
23
|
+
"aws_secret_access_key=[REDACTED_AWS_SECRET]",
|
|
24
|
+
),
|
|
25
|
+
(
|
|
26
|
+
re.compile(r"(?i)\bAuthorization\s*[:=]\s*Bearer\s+[A-Za-z0-9._~+/-]+=*"),
|
|
27
|
+
"Authorization: Bearer [REDACTED_BEARER_TOKEN]",
|
|
28
|
+
),
|
|
29
|
+
(re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/-]+=*"), "Bearer [REDACTED_BEARER_TOKEN]"),
|
|
30
|
+
(
|
|
31
|
+
re.compile(r"\bxox(?:a|b|p|r|s)-[A-Za-z0-9-]{10,}\b"),
|
|
32
|
+
"[REDACTED_SLACK_TOKEN]",
|
|
33
|
+
),
|
|
34
|
+
(re.compile(r"\bxapp-[A-Za-z0-9-]{10,}\b"), "[REDACTED_SLACK_TOKEN]"),
|
|
35
|
+
(
|
|
36
|
+
re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
|
|
37
|
+
"[REDACTED_JWT]",
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
re.compile(
|
|
41
|
+
r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
|
|
42
|
+
re.S,
|
|
43
|
+
),
|
|
44
|
+
"[REDACTED_PRIVATE_KEY]",
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
re.compile(
|
|
48
|
+
r"(?i)\b([A-Z0-9_ -]*(?:password|api[_-]?key|token|secret|credential|private[_-]?key)[A-Z0-9_ -]*)\s*[:=]\s*"
|
|
49
|
+
r"(['\"]?)[^'\"\s,;}]+\2"
|
|
50
|
+
),
|
|
51
|
+
r"\1=[REDACTED_SECRET]",
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_OUTPUT_OMITTED = (
|
|
56
|
+
"Tool output omitted by default. Reload with include_tool_output=true to inspect "
|
|
57
|
+
"redacted, size-limited output."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_call_context(
|
|
62
|
+
record_id: str,
|
|
63
|
+
db_path: Path = DEFAULT_DB_PATH,
|
|
64
|
+
max_chars: int = DEFAULT_CONTEXT_CHARS,
|
|
65
|
+
max_entries: int = DEFAULT_CONTEXT_ENTRIES,
|
|
66
|
+
include_tool_output: bool = False,
|
|
67
|
+
) -> dict[str, Any]:
|
|
68
|
+
"""Load logged turn context for one model call from the source JSONL file.
|
|
69
|
+
|
|
70
|
+
This intentionally reads raw transcript-like data only on demand. The returned
|
|
71
|
+
context is not written back to SQLite or embedded in dashboard HTML.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
row = query_usage_record(db_path=db_path, record_id=record_id)
|
|
75
|
+
if row is None:
|
|
76
|
+
raise ValueError(f"No usage record found for record_id: {record_id}")
|
|
77
|
+
|
|
78
|
+
source_file = Path(str(row.get("source_file") or ""))
|
|
79
|
+
if not source_file.exists():
|
|
80
|
+
raise FileNotFoundError(f"Source log not found: {source_file}")
|
|
81
|
+
|
|
82
|
+
line_number = _positive_int(row.get("line_number"))
|
|
83
|
+
if line_number is None:
|
|
84
|
+
raise ValueError(f"Usage record has no valid source line: {record_id}")
|
|
85
|
+
|
|
86
|
+
target_turn_id = _optional_str(row.get("turn_id"))
|
|
87
|
+
entries, omitted = _read_context_entries(
|
|
88
|
+
path=source_file,
|
|
89
|
+
token_line=line_number,
|
|
90
|
+
target_turn_id=target_turn_id,
|
|
91
|
+
max_chars=max(1_000, max_chars),
|
|
92
|
+
max_entries=max(1, max_entries),
|
|
93
|
+
include_tool_output=include_tool_output,
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
"schema": "codex-usage-tracker-context-v1",
|
|
97
|
+
"loaded_on_demand": True,
|
|
98
|
+
"raw_context_persisted": False,
|
|
99
|
+
"include_tool_output": include_tool_output,
|
|
100
|
+
"record": {
|
|
101
|
+
"record_id": row.get("record_id"),
|
|
102
|
+
"session_id": row.get("session_id"),
|
|
103
|
+
"thread_name": row.get("thread_name"),
|
|
104
|
+
"turn_id": row.get("turn_id"),
|
|
105
|
+
"event_timestamp": row.get("event_timestamp"),
|
|
106
|
+
"model": row.get("model"),
|
|
107
|
+
"effort": row.get("effort"),
|
|
108
|
+
"parent_session_id": row.get("parent_session_id"),
|
|
109
|
+
"parent_thread_name": row.get("parent_thread_name"),
|
|
110
|
+
"total_tokens": row.get("total_tokens"),
|
|
111
|
+
"cumulative_total_tokens": row.get("cumulative_total_tokens"),
|
|
112
|
+
},
|
|
113
|
+
"source": {
|
|
114
|
+
"file": str(source_file),
|
|
115
|
+
"line_number": line_number,
|
|
116
|
+
},
|
|
117
|
+
"entries": entries,
|
|
118
|
+
"omitted": omitted,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _read_context_entries(
|
|
123
|
+
path: Path,
|
|
124
|
+
token_line: int,
|
|
125
|
+
target_turn_id: str | None,
|
|
126
|
+
max_chars: int,
|
|
127
|
+
max_entries: int,
|
|
128
|
+
include_tool_output: bool,
|
|
129
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
130
|
+
candidates: list[dict[str, Any]] = []
|
|
131
|
+
omitted_parse_errors = 0
|
|
132
|
+
current_turn_id: str | None = None
|
|
133
|
+
collecting = target_turn_id is None
|
|
134
|
+
|
|
135
|
+
with path.open(encoding="utf-8") as handle:
|
|
136
|
+
for line_number, line in enumerate(handle, 1):
|
|
137
|
+
if line_number > token_line:
|
|
138
|
+
break
|
|
139
|
+
try:
|
|
140
|
+
envelope = json.loads(line)
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
omitted_parse_errors += 1
|
|
143
|
+
continue
|
|
144
|
+
if not isinstance(envelope, dict):
|
|
145
|
+
continue
|
|
146
|
+
entry_type = _optional_str(envelope.get("type")) or "unknown"
|
|
147
|
+
payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {}
|
|
148
|
+
timestamp = _optional_str(envelope.get("timestamp"))
|
|
149
|
+
|
|
150
|
+
if entry_type == "turn_context":
|
|
151
|
+
current_turn_id = _optional_str(payload.get("turn_id"))
|
|
152
|
+
collecting = target_turn_id is None or current_turn_id == target_turn_id
|
|
153
|
+
if collecting:
|
|
154
|
+
candidates = []
|
|
155
|
+
candidates.append(
|
|
156
|
+
_context_entry(
|
|
157
|
+
line_number,
|
|
158
|
+
timestamp,
|
|
159
|
+
entry_type,
|
|
160
|
+
"Turn context",
|
|
161
|
+
_summarize_turn_context(payload),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if not collecting:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
summarized = _summarize_payload(
|
|
170
|
+
entry_type=entry_type,
|
|
171
|
+
payload=payload,
|
|
172
|
+
include_tool_output=include_tool_output,
|
|
173
|
+
)
|
|
174
|
+
if summarized is not None:
|
|
175
|
+
candidates.append(
|
|
176
|
+
_context_entry(
|
|
177
|
+
line_number,
|
|
178
|
+
timestamp,
|
|
179
|
+
entry_type,
|
|
180
|
+
summarized["label"],
|
|
181
|
+
summarized["text"],
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
line_number >= token_line
|
|
187
|
+
and entry_type == "event_msg"
|
|
188
|
+
and payload.get("type") == "token_count"
|
|
189
|
+
):
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
limited, omitted = _limit_entries(candidates, max_chars=max_chars, max_entries=max_entries)
|
|
193
|
+
omitted["parse_errors"] = omitted_parse_errors
|
|
194
|
+
omitted["target_turn_id"] = target_turn_id
|
|
195
|
+
return limited, omitted
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _summarize_payload(
|
|
199
|
+
entry_type: str,
|
|
200
|
+
payload: dict[str, Any],
|
|
201
|
+
include_tool_output: bool,
|
|
202
|
+
) -> dict[str, str] | None:
|
|
203
|
+
if entry_type == "response_item":
|
|
204
|
+
return _summarize_response_item(payload, include_tool_output=include_tool_output)
|
|
205
|
+
if entry_type == "event_msg":
|
|
206
|
+
return _summarize_event_msg(payload, include_tool_output=include_tool_output)
|
|
207
|
+
if entry_type == "compacted":
|
|
208
|
+
message = _optional_str(payload.get("message")) or "Compaction event"
|
|
209
|
+
return {"label": "Compaction", "text": message}
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _summarize_turn_context(payload: dict[str, Any]) -> str:
|
|
214
|
+
fields = [
|
|
215
|
+
("turn_id", payload.get("turn_id")),
|
|
216
|
+
("cwd", payload.get("cwd")),
|
|
217
|
+
("model", payload.get("model")),
|
|
218
|
+
("effort", payload.get("effort")),
|
|
219
|
+
("current_date", payload.get("current_date")),
|
|
220
|
+
("timezone", payload.get("timezone")),
|
|
221
|
+
]
|
|
222
|
+
lines = [f"{key}: {value}" for key, value in fields if value not in (None, "")]
|
|
223
|
+
summary = _optional_str(payload.get("summary"))
|
|
224
|
+
if summary:
|
|
225
|
+
lines.append(f"summary: {summary}")
|
|
226
|
+
return "\n".join(lines) if lines else "Turn context"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _summarize_response_item(
|
|
230
|
+
payload: dict[str, Any],
|
|
231
|
+
include_tool_output: bool,
|
|
232
|
+
) -> dict[str, str] | None:
|
|
233
|
+
item_type = _optional_str(payload.get("type")) or "response_item"
|
|
234
|
+
role = _optional_str(payload.get("role"))
|
|
235
|
+
name = _optional_str(payload.get("name"))
|
|
236
|
+
label_bits = [item_type]
|
|
237
|
+
if role:
|
|
238
|
+
label_bits.append(role)
|
|
239
|
+
if name:
|
|
240
|
+
label_bits.append(name)
|
|
241
|
+
label = " / ".join(label_bits)
|
|
242
|
+
|
|
243
|
+
content_text = _content_text(payload.get("content"))
|
|
244
|
+
if content_text:
|
|
245
|
+
return {"label": label, "text": content_text}
|
|
246
|
+
|
|
247
|
+
if "arguments" in payload:
|
|
248
|
+
return {
|
|
249
|
+
"label": label,
|
|
250
|
+
"text": f"Tool call arguments:\n{_jsonish(payload.get('arguments'))}",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if "input" in payload:
|
|
254
|
+
return {
|
|
255
|
+
"label": label,
|
|
256
|
+
"text": f"Tool input:\n{_jsonish(payload.get('input'))}",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if "output" in payload:
|
|
260
|
+
output = _optional_str(payload.get("output")) or _jsonish(payload.get("output"))
|
|
261
|
+
return {
|
|
262
|
+
"label": label,
|
|
263
|
+
"text": output if include_tool_output else _OUTPUT_OMITTED,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
summary = _content_text(payload.get("summary"))
|
|
267
|
+
if summary:
|
|
268
|
+
return {"label": label, "text": summary}
|
|
269
|
+
|
|
270
|
+
action = payload.get("action")
|
|
271
|
+
if isinstance(action, dict):
|
|
272
|
+
return {"label": label, "text": f"Action:\n{_jsonish(action)}"}
|
|
273
|
+
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _summarize_event_msg(
|
|
278
|
+
payload: dict[str, Any],
|
|
279
|
+
include_tool_output: bool,
|
|
280
|
+
) -> dict[str, str] | None:
|
|
281
|
+
event_type = _optional_str(payload.get("type")) or "event_msg"
|
|
282
|
+
if event_type == "token_count":
|
|
283
|
+
info = payload.get("info") if isinstance(payload.get("info"), dict) else {}
|
|
284
|
+
return {"label": "Token count", "text": _jsonish(_token_count_summary(info))}
|
|
285
|
+
|
|
286
|
+
if "message" in payload:
|
|
287
|
+
return {"label": event_type, "text": _optional_str(payload.get("message")) or ""}
|
|
288
|
+
|
|
289
|
+
output_fields = [field for field in ("stdout", "stderr", "result") if field in payload]
|
|
290
|
+
if output_fields:
|
|
291
|
+
if not include_tool_output:
|
|
292
|
+
return {"label": event_type, "text": _OUTPUT_OMITTED}
|
|
293
|
+
text = "\n".join(f"{field}:\n{_jsonish(payload.get(field))}" for field in output_fields)
|
|
294
|
+
return {"label": event_type, "text": text}
|
|
295
|
+
|
|
296
|
+
compact = {
|
|
297
|
+
key: payload.get(key)
|
|
298
|
+
for key in ("call_id", "turn_id", "phase", "status", "duration_ms")
|
|
299
|
+
if key in payload
|
|
300
|
+
}
|
|
301
|
+
return {"label": event_type, "text": _jsonish(compact)} if compact else None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _token_count_summary(info: dict[str, Any]) -> dict[str, Any]:
|
|
305
|
+
return {
|
|
306
|
+
"last_token_usage": info.get("last_token_usage"),
|
|
307
|
+
"total_token_usage": info.get("total_token_usage"),
|
|
308
|
+
"model_context_window": info.get("model_context_window"),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _context_entry(
|
|
313
|
+
line_number: int,
|
|
314
|
+
timestamp: str | None,
|
|
315
|
+
entry_type: str,
|
|
316
|
+
label: str,
|
|
317
|
+
text: str,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
return {
|
|
320
|
+
"line_number": line_number,
|
|
321
|
+
"timestamp": timestamp,
|
|
322
|
+
"type": entry_type,
|
|
323
|
+
"label": label,
|
|
324
|
+
"text": _redact(text),
|
|
325
|
+
"truncated": False,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _limit_entries(
|
|
330
|
+
entries: list[dict[str, Any]],
|
|
331
|
+
max_chars: int,
|
|
332
|
+
max_entries: int,
|
|
333
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
334
|
+
limited_reversed: list[dict[str, Any]] = []
|
|
335
|
+
remaining = max_chars
|
|
336
|
+
omitted_entries = 0
|
|
337
|
+
omitted_chars = 0
|
|
338
|
+
selected = entries[-max_entries:]
|
|
339
|
+
|
|
340
|
+
for entry in reversed(selected):
|
|
341
|
+
text = str(entry.get("text") or "")
|
|
342
|
+
if remaining <= 0:
|
|
343
|
+
omitted_entries += 1
|
|
344
|
+
omitted_chars += len(text)
|
|
345
|
+
continue
|
|
346
|
+
if len(text) > remaining:
|
|
347
|
+
entry = dict(entry)
|
|
348
|
+
entry["text"] = text[:remaining] + "\n[TRUNCATED]"
|
|
349
|
+
entry["truncated"] = True
|
|
350
|
+
omitted_chars += len(text) - remaining
|
|
351
|
+
remaining = 0
|
|
352
|
+
else:
|
|
353
|
+
remaining -= len(text)
|
|
354
|
+
limited_reversed.append(entry)
|
|
355
|
+
|
|
356
|
+
limited = list(reversed(limited_reversed))
|
|
357
|
+
return limited, {
|
|
358
|
+
"older_entries": max(0, len(entries) - max_entries),
|
|
359
|
+
"over_budget_entries": omitted_entries,
|
|
360
|
+
"over_budget_chars": omitted_chars,
|
|
361
|
+
"max_chars": max_chars,
|
|
362
|
+
"max_entries": max_entries,
|
|
363
|
+
"returned_entries": len(limited),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _content_text(value: object) -> str:
|
|
368
|
+
if value is None:
|
|
369
|
+
return ""
|
|
370
|
+
if isinstance(value, str):
|
|
371
|
+
return value
|
|
372
|
+
if isinstance(value, list):
|
|
373
|
+
pieces: list[str] = []
|
|
374
|
+
for item in value:
|
|
375
|
+
if isinstance(item, str):
|
|
376
|
+
pieces.append(item)
|
|
377
|
+
elif isinstance(item, dict):
|
|
378
|
+
text = item.get("text") or item.get("content")
|
|
379
|
+
if isinstance(text, str):
|
|
380
|
+
pieces.append(text)
|
|
381
|
+
return "\n".join(piece for piece in pieces if piece)
|
|
382
|
+
return _jsonish(value)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _jsonish(value: object) -> str:
|
|
386
|
+
if isinstance(value, str):
|
|
387
|
+
return value
|
|
388
|
+
try:
|
|
389
|
+
return json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True)
|
|
390
|
+
except TypeError:
|
|
391
|
+
return str(value)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _redact(text: str) -> str:
|
|
395
|
+
redacted = text
|
|
396
|
+
for pattern, replacement in _SECRET_PATTERNS:
|
|
397
|
+
redacted = pattern.sub(replacement, redacted)
|
|
398
|
+
return redacted
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _positive_int(value: object) -> int | None:
|
|
402
|
+
try:
|
|
403
|
+
number = int(value) # type: ignore[arg-type]
|
|
404
|
+
except (TypeError, ValueError):
|
|
405
|
+
return None
|
|
406
|
+
return number if number > 0 else None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _optional_str(value: object) -> str | None:
|
|
410
|
+
return value if isinstance(value, str) and value else None
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Cost estimation and pricing coverage calculations for aggregate usage rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from codex_usage_tracker.paths import DEFAULT_PRICING_PATH
|
|
9
|
+
from codex_usage_tracker.pricing_config import PricingConfig, load_pricing_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def summarize_pricing_coverage(
|
|
13
|
+
rows: list[dict[str, Any]],
|
|
14
|
+
pricing: PricingConfig | None = None,
|
|
15
|
+
*,
|
|
16
|
+
model_field: str = "group_key",
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
"""Summarize which aggregate model rows have usable local pricing."""
|
|
19
|
+
|
|
20
|
+
config = pricing or load_pricing_config()
|
|
21
|
+
coverage_rows: list[dict[str, Any]] = []
|
|
22
|
+
totals = {
|
|
23
|
+
"model_count": 0,
|
|
24
|
+
"priced_model_count": 0,
|
|
25
|
+
"unpriced_model_count": 0,
|
|
26
|
+
"total_tokens": 0.0,
|
|
27
|
+
"priced_tokens": 0.0,
|
|
28
|
+
"unpriced_tokens": 0.0,
|
|
29
|
+
"estimated_cost_usd": 0.0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for row in rows:
|
|
33
|
+
model = row.get(model_field)
|
|
34
|
+
priced_as = config.priced_as(model)
|
|
35
|
+
copy = dict(row)
|
|
36
|
+
copy["model"] = model
|
|
37
|
+
copy["priced"] = priced_as is not None
|
|
38
|
+
copy["priced_as"] = priced_as
|
|
39
|
+
copy["pricing_estimated"] = config.is_estimated_model(model)
|
|
40
|
+
copy["estimated_cost_usd"] = estimate_cost_usd(copy, config, model=model)
|
|
41
|
+
total_tokens = _number(copy.get("total_tokens"))
|
|
42
|
+
totals["model_count"] += 1
|
|
43
|
+
totals["total_tokens"] += total_tokens
|
|
44
|
+
if priced_as:
|
|
45
|
+
totals["priced_model_count"] += 1
|
|
46
|
+
totals["priced_tokens"] += total_tokens
|
|
47
|
+
else:
|
|
48
|
+
totals["unpriced_model_count"] += 1
|
|
49
|
+
totals["unpriced_tokens"] += total_tokens
|
|
50
|
+
if isinstance(copy["estimated_cost_usd"], int | float):
|
|
51
|
+
totals["estimated_cost_usd"] += float(copy["estimated_cost_usd"])
|
|
52
|
+
coverage_rows.append(copy)
|
|
53
|
+
|
|
54
|
+
total_tokens = totals["total_tokens"]
|
|
55
|
+
totals["priced_token_ratio"] = (
|
|
56
|
+
totals["priced_tokens"] / total_tokens if total_tokens else 0.0
|
|
57
|
+
)
|
|
58
|
+
coverage_rows.sort(
|
|
59
|
+
key=lambda row: (
|
|
60
|
+
0 if row.get("priced") is False else 1,
|
|
61
|
+
-_number(row.get("total_tokens")),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return {
|
|
65
|
+
"schema": "codex-usage-tracker-pricing-coverage-v1",
|
|
66
|
+
**totals,
|
|
67
|
+
"pricing_loaded": config.loaded and not config.error,
|
|
68
|
+
"pricing_path": str(config.path),
|
|
69
|
+
"pricing_source": config.source,
|
|
70
|
+
"rows": coverage_rows,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def annotate_rows_with_efficiency(
|
|
75
|
+
rows: list[dict[str, Any]],
|
|
76
|
+
pricing: PricingConfig | None = None,
|
|
77
|
+
*,
|
|
78
|
+
model_field: str = "model",
|
|
79
|
+
pricing_path: Path = DEFAULT_PRICING_PATH,
|
|
80
|
+
) -> list[dict[str, Any]]:
|
|
81
|
+
"""Return copied rows with local cost estimates and efficiency flags."""
|
|
82
|
+
|
|
83
|
+
config = pricing or load_pricing_config(pricing_path)
|
|
84
|
+
annotated: list[dict[str, Any]] = []
|
|
85
|
+
for row in rows:
|
|
86
|
+
copy = dict(row)
|
|
87
|
+
model = copy.get(model_field)
|
|
88
|
+
cost = estimate_cost_usd(copy, config, model=model)
|
|
89
|
+
savings = estimate_cache_savings_usd(copy, config, model=model)
|
|
90
|
+
copy["estimated_cost_usd"] = cost
|
|
91
|
+
copy["estimated_cache_savings_usd"] = savings
|
|
92
|
+
copy["pricing_model"] = config.priced_as(model)
|
|
93
|
+
copy["pricing_estimated"] = config.is_estimated_model(model)
|
|
94
|
+
copy["efficiency_flags"] = efficiency_flags(copy)
|
|
95
|
+
annotated.append(copy)
|
|
96
|
+
return annotated
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def estimate_cost_usd(
|
|
100
|
+
row: dict[str, Any], pricing: PricingConfig, *, model: object | None = None
|
|
101
|
+
) -> float | None:
|
|
102
|
+
"""Estimate call cost from aggregate tokens and local model rates."""
|
|
103
|
+
|
|
104
|
+
rates = pricing.rates_for(model if model is not None else row.get("model"))
|
|
105
|
+
if not rates:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
input_rate = rates.get("input_per_million")
|
|
109
|
+
cached_rate = rates.get("cached_input_per_million", input_rate)
|
|
110
|
+
output_rate = rates.get("output_per_million")
|
|
111
|
+
if input_rate is None or cached_rate is None or output_rate is None:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
cached_input = _number(row.get("cached_input_tokens"))
|
|
115
|
+
uncached_input = _number(row.get("uncached_input_tokens"))
|
|
116
|
+
if uncached_input <= 0:
|
|
117
|
+
uncached_input = max(_number(row.get("input_tokens")) - cached_input, 0.0)
|
|
118
|
+
output_tokens = _number(row.get("output_tokens"))
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
(uncached_input * input_rate)
|
|
122
|
+
+ (cached_input * cached_rate)
|
|
123
|
+
+ (output_tokens * output_rate)
|
|
124
|
+
) / 1_000_000
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def estimate_cache_savings_usd(
|
|
128
|
+
row: dict[str, Any], pricing: PricingConfig, *, model: object | None = None
|
|
129
|
+
) -> float | None:
|
|
130
|
+
"""Estimate local cache savings when cached input has a lower configured rate."""
|
|
131
|
+
|
|
132
|
+
rates = pricing.rates_for(model if model is not None else row.get("model"))
|
|
133
|
+
if not rates:
|
|
134
|
+
return None
|
|
135
|
+
input_rate = rates.get("input_per_million")
|
|
136
|
+
cached_rate = rates.get("cached_input_per_million")
|
|
137
|
+
if input_rate is None or cached_rate is None or cached_rate >= input_rate:
|
|
138
|
+
return None
|
|
139
|
+
return (_number(row.get("cached_input_tokens")) * (input_rate - cached_rate)) / 1_000_000
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def efficiency_flags(row: dict[str, Any]) -> list[str]:
|
|
143
|
+
"""Generate aggregate-only signals worth reviewing."""
|
|
144
|
+
|
|
145
|
+
flags: list[str] = []
|
|
146
|
+
total_tokens = _number(row.get("total_tokens"))
|
|
147
|
+
output_tokens = _number(row.get("output_tokens"))
|
|
148
|
+
input_tokens = _number(row.get("input_tokens"))
|
|
149
|
+
context = _number(row.get("context_window_percent"))
|
|
150
|
+
cache = _number(row.get("cache_ratio"))
|
|
151
|
+
reasoning = _number(row.get("reasoning_output_ratio"))
|
|
152
|
+
cost = row.get("estimated_cost_usd")
|
|
153
|
+
|
|
154
|
+
if context >= 0.8:
|
|
155
|
+
flags.append("high context use")
|
|
156
|
+
elif context >= 0.5:
|
|
157
|
+
flags.append("elevated context use")
|
|
158
|
+
if reasoning >= 0.75 and output_tokens >= 100:
|
|
159
|
+
flags.append("high reasoning share")
|
|
160
|
+
if input_tokens >= 10_000 and cache < 0.1:
|
|
161
|
+
flags.append("low cache reuse")
|
|
162
|
+
if total_tokens >= 20_000 and output_tokens <= 100:
|
|
163
|
+
flags.append("expensive low-output call")
|
|
164
|
+
if isinstance(cost, int | float) and cost >= 1:
|
|
165
|
+
flags.append("high estimated cost")
|
|
166
|
+
return flags
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _number(value: object) -> float:
|
|
170
|
+
if isinstance(value, bool):
|
|
171
|
+
return float(int(value))
|
|
172
|
+
if isinstance(value, int | float):
|
|
173
|
+
return float(value)
|
|
174
|
+
if isinstance(value, str) and value.strip():
|
|
175
|
+
return float(value)
|
|
176
|
+
return 0.0
|