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.
Files changed (50) hide show
  1. codex_usage_tracker/__init__.py +7 -0
  2. codex_usage_tracker/__main__.py +6 -0
  3. codex_usage_tracker/allowance.py +759 -0
  4. codex_usage_tracker/api_payloads.py +90 -0
  5. codex_usage_tracker/cli.py +1326 -0
  6. codex_usage_tracker/context.py +410 -0
  7. codex_usage_tracker/costing.py +176 -0
  8. codex_usage_tracker/dashboard.py +389 -0
  9. codex_usage_tracker/diagnostics.py +624 -0
  10. codex_usage_tracker/formatting.py +225 -0
  11. codex_usage_tracker/json_contracts.py +350 -0
  12. codex_usage_tracker/mcp_server.py +371 -0
  13. codex_usage_tracker/models.py +92 -0
  14. codex_usage_tracker/parser.py +491 -0
  15. codex_usage_tracker/paths.py +18 -0
  16. codex_usage_tracker/plugin_data/__init__.py +1 -0
  17. codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
  18. codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
  19. codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
  20. codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
  21. codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
  22. codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
  23. codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
  24. codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
  25. codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
  26. codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
  27. codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
  28. codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
  29. codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
  30. codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
  31. codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
  32. codex_usage_tracker/plugin_installer.py +312 -0
  33. codex_usage_tracker/pricing.py +57 -0
  34. codex_usage_tracker/pricing_config.py +223 -0
  35. codex_usage_tracker/pricing_estimates.py +44 -0
  36. codex_usage_tracker/pricing_openai.py +253 -0
  37. codex_usage_tracker/projects.py +347 -0
  38. codex_usage_tracker/recommendations.py +270 -0
  39. codex_usage_tracker/reports.py +637 -0
  40. codex_usage_tracker/schema.py +71 -0
  41. codex_usage_tracker/server.py +400 -0
  42. codex_usage_tracker/store.py +666 -0
  43. codex_usage_tracker/support.py +147 -0
  44. codex_usage_tracker/threads.py +183 -0
  45. codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
  46. codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
  47. codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
  48. codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
  49. codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
  50. codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,371 @@
1
+ """MCP server exposing aggregate-only Codex usage tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+
12
+ from codex_usage_tracker.allowance import write_allowance_template
13
+ from codex_usage_tracker.api_payloads import refresh_result_payload, session_payload
14
+ from codex_usage_tracker.context import DEFAULT_CONTEXT_CHARS, load_call_context
15
+ from codex_usage_tracker.dashboard import generate_dashboard
16
+ from codex_usage_tracker.diagnostics import run_doctor
17
+ from codex_usage_tracker.formatting import (
18
+ format_doctor,
19
+ format_session,
20
+ )
21
+ from codex_usage_tracker.paths import (
22
+ DEFAULT_ALLOWANCE_PATH,
23
+ DEFAULT_CODEX_HOME,
24
+ DEFAULT_DASHBOARD_PATH,
25
+ DEFAULT_DB_PATH,
26
+ DEFAULT_PRICING_PATH,
27
+ DEFAULT_PROJECTS_PATH,
28
+ )
29
+ from codex_usage_tracker.pricing import (
30
+ update_pricing_from_openai_docs,
31
+ write_pricing_template,
32
+ )
33
+ from codex_usage_tracker.projects import apply_project_privacy_to_rows
34
+ from codex_usage_tracker.reports import (
35
+ build_expensive_calls_report,
36
+ build_pricing_coverage_report,
37
+ build_query_report,
38
+ build_recommendations_report,
39
+ build_summary_report,
40
+ )
41
+ from codex_usage_tracker.store import (
42
+ export_usage_csv as export_csv,
43
+ )
44
+ from codex_usage_tracker.store import (
45
+ query_session_usage,
46
+ )
47
+ from codex_usage_tracker.store import (
48
+ refresh_usage_index as refresh_index,
49
+ )
50
+
51
+ mcp = FastMCP("codex-usage-tracker")
52
+
53
+
54
+ @mcp.tool()
55
+ def refresh_usage_index(include_archived: bool = False) -> dict[str, Any]:
56
+ """Scan local Codex logs and upsert aggregate usage metrics into SQLite."""
57
+
58
+ result = refresh_index(
59
+ codex_home=DEFAULT_CODEX_HOME,
60
+ db_path=DEFAULT_DB_PATH,
61
+ include_archived=include_archived,
62
+ )
63
+ return refresh_result_payload(result, schema="codex-usage-tracker-refresh-v1")
64
+
65
+
66
+ @mcp.tool()
67
+ def usage_doctor(response_format: str = "markdown") -> str | dict[str, Any]:
68
+ """Check the local plugin, MCP, database, dashboard, and pricing setup."""
69
+
70
+ report = run_doctor(db_path=DEFAULT_DB_PATH, pricing_path=DEFAULT_PRICING_PATH)
71
+ if response_format == "json":
72
+ return report
73
+ return format_doctor(report)
74
+
75
+
76
+ @mcp.tool()
77
+ def usage_summary(
78
+ group_by: str = "thread",
79
+ limit: int = 20,
80
+ preset: str | None = None,
81
+ since: str | None = None,
82
+ response_format: str = "markdown",
83
+ privacy_mode: str = "normal",
84
+ ) -> str | dict[str, Any]:
85
+ """Summarize aggregate Codex token usage by date, model, effort, cwd, thread, session, parent thread, or subagent metadata."""
86
+
87
+ report = build_summary_report(
88
+ db_path=DEFAULT_DB_PATH,
89
+ pricing_path=DEFAULT_PRICING_PATH,
90
+ group_by=group_by,
91
+ limit=limit,
92
+ preset=preset,
93
+ since=since,
94
+ privacy_mode=privacy_mode,
95
+ )
96
+ if response_format == "json":
97
+ return report.payload()
98
+ return report.render()
99
+
100
+
101
+ @mcp.tool()
102
+ def session_usage(
103
+ session_id: str | None = None,
104
+ limit: int = 200,
105
+ response_format: str = "markdown",
106
+ privacy_mode: str = "normal",
107
+ ) -> str | dict[str, Any]:
108
+ """Show aggregate per-call usage for one session, defaulting to the latest indexed session."""
109
+
110
+ rows = apply_project_privacy_to_rows(
111
+ query_session_usage(DEFAULT_DB_PATH, session_id=session_id, limit=limit),
112
+ privacy_mode=privacy_mode,
113
+ )
114
+ if response_format == "json":
115
+ return session_payload(
116
+ rows,
117
+ requested_session_id=session_id,
118
+ limit=limit,
119
+ privacy_mode=privacy_mode,
120
+ )
121
+ return format_session(rows)
122
+
123
+
124
+ @mcp.tool()
125
+ def usage_call_context(
126
+ record_id: str,
127
+ max_chars: int = DEFAULT_CONTEXT_CHARS,
128
+ include_tool_output: bool = False,
129
+ ) -> str:
130
+ """Load one model call's logged local context on demand from its source JSONL file."""
131
+
132
+ if os.environ.get("CODEX_USAGE_TRACKER_ALLOW_RAW_CONTEXT") != "1":
133
+ return json.dumps(
134
+ {
135
+ "schema": "codex-usage-tracker-context-disabled-v1",
136
+ "error": (
137
+ "Raw context loading through MCP is disabled. Set "
138
+ "CODEX_USAGE_TRACKER_ALLOW_RAW_CONTEXT=1 to opt in for this process."
139
+ ),
140
+ "raw_context_enabled": False,
141
+ "record_id": record_id,
142
+ },
143
+ indent=2,
144
+ )
145
+ payload = load_call_context(
146
+ record_id=record_id,
147
+ db_path=DEFAULT_DB_PATH,
148
+ max_chars=max_chars,
149
+ include_tool_output=include_tool_output,
150
+ )
151
+ return json.dumps(payload, indent=2)
152
+
153
+
154
+ @mcp.tool()
155
+ def most_expensive_usage_calls(
156
+ limit: int = 20,
157
+ preset: str | None = None,
158
+ since: str | None = None,
159
+ response_format: str = "markdown",
160
+ privacy_mode: str = "normal",
161
+ ) -> str | dict[str, Any]:
162
+ """Show the highest last-call aggregate usage rows with efficiency signals."""
163
+
164
+ report = build_expensive_calls_report(
165
+ db_path=DEFAULT_DB_PATH,
166
+ pricing_path=DEFAULT_PRICING_PATH,
167
+ limit=limit,
168
+ preset=preset,
169
+ since=since,
170
+ privacy_mode=privacy_mode,
171
+ )
172
+ if response_format == "json":
173
+ return report.payload()
174
+ return report.render()
175
+
176
+
177
+ @mcp.tool()
178
+ def usage_query(
179
+ since: str | None = None,
180
+ until: str | None = None,
181
+ model: str | None = None,
182
+ effort: str | None = None,
183
+ thread: str | None = None,
184
+ project: str | None = None,
185
+ pricing_status: str | None = None,
186
+ credit_confidence: str | None = None,
187
+ min_tokens: int | None = None,
188
+ min_credits: float | None = None,
189
+ limit: int = 100,
190
+ privacy_mode: str = "normal",
191
+ ) -> dict[str, Any]:
192
+ """Return stable JSON aggregate usage rows with filters for automation."""
193
+
194
+ return build_query_report(
195
+ db_path=DEFAULT_DB_PATH,
196
+ pricing_path=DEFAULT_PRICING_PATH,
197
+ allowance_path=DEFAULT_ALLOWANCE_PATH,
198
+ projects_path=DEFAULT_PROJECTS_PATH,
199
+ since=since,
200
+ until=until,
201
+ model=model,
202
+ effort=effort,
203
+ thread=thread,
204
+ project=project,
205
+ pricing_status=pricing_status,
206
+ credit_confidence=credit_confidence,
207
+ min_tokens=min_tokens,
208
+ min_credits=min_credits,
209
+ limit=limit,
210
+ privacy_mode=privacy_mode,
211
+ ).payload
212
+
213
+
214
+ @mcp.tool()
215
+ def usage_recommendations(
216
+ since: str | None = None,
217
+ until: str | None = None,
218
+ model: str | None = None,
219
+ effort: str | None = None,
220
+ thread: str | None = None,
221
+ project: str | None = None,
222
+ min_score: float | None = None,
223
+ limit: int = 20,
224
+ response_format: str = "markdown",
225
+ privacy_mode: str = "normal",
226
+ ) -> str | dict[str, Any]:
227
+ """Rank aggregate usage rows and threads by recommendation severity."""
228
+
229
+ report = build_recommendations_report(
230
+ db_path=DEFAULT_DB_PATH,
231
+ pricing_path=DEFAULT_PRICING_PATH,
232
+ allowance_path=DEFAULT_ALLOWANCE_PATH,
233
+ projects_path=DEFAULT_PROJECTS_PATH,
234
+ since=since,
235
+ until=until,
236
+ model=model,
237
+ effort=effort,
238
+ thread=thread,
239
+ project=project,
240
+ min_score=min_score,
241
+ limit=limit,
242
+ privacy_mode=privacy_mode,
243
+ )
244
+ if response_format == "json":
245
+ return report.payload
246
+ return report.render()
247
+
248
+
249
+ @mcp.tool()
250
+ def usage_pricing_coverage(
251
+ limit: int = 20,
252
+ since: str | None = None,
253
+ response_format: str = "markdown",
254
+ ) -> str | dict[str, Any]:
255
+ """Show priced, estimated, and unpriced token coverage by model."""
256
+
257
+ report = build_pricing_coverage_report(
258
+ db_path=DEFAULT_DB_PATH,
259
+ pricing_path=DEFAULT_PRICING_PATH,
260
+ since=since,
261
+ )
262
+ if response_format == "json":
263
+ return report.payload
264
+ return report.render(limit=limit)
265
+
266
+
267
+ @mcp.tool()
268
+ def generate_usage_dashboard(
269
+ output_path: str | None = None,
270
+ limit: int = 5000,
271
+ since: str | None = None,
272
+ privacy_mode: str = "normal",
273
+ include_archived: bool = False,
274
+ ) -> dict[str, Any]:
275
+ """Generate a local hoverable HTML dashboard from aggregate-only usage metrics."""
276
+
277
+ output = Path(output_path).expanduser() if output_path else DEFAULT_DASHBOARD_PATH
278
+ generated = generate_dashboard(
279
+ DEFAULT_DB_PATH,
280
+ output_path=output,
281
+ limit=limit,
282
+ pricing_path=DEFAULT_PRICING_PATH,
283
+ allowance_path=DEFAULT_ALLOWANCE_PATH,
284
+ since=since,
285
+ privacy_mode=privacy_mode,
286
+ include_archived=include_archived,
287
+ )
288
+ return {
289
+ "schema": "codex-usage-tracker-dashboard-v1",
290
+ "dashboard_path": str(generated),
291
+ "file_url": generated.resolve().as_uri(),
292
+ "opened": False,
293
+ "limit": None if limit <= 0 else limit,
294
+ "since": since,
295
+ "privacy_mode": privacy_mode,
296
+ "include_archived": include_archived,
297
+ }
298
+
299
+
300
+ @mcp.tool()
301
+ def export_usage_csv(
302
+ output_path: str,
303
+ limit: int | None = None,
304
+ privacy_mode: str = "normal",
305
+ ) -> dict[str, Any]:
306
+ """Export aggregate Codex token usage rows to a local CSV file."""
307
+
308
+ output = Path(output_path).expanduser()
309
+ rows = export_csv(
310
+ output_path=output,
311
+ db_path=DEFAULT_DB_PATH,
312
+ limit=limit,
313
+ privacy_mode=privacy_mode,
314
+ )
315
+ return {
316
+ "schema": "codex-usage-tracker-export-v1",
317
+ "rows": rows,
318
+ "csv_path": str(output),
319
+ "limit": limit,
320
+ "privacy_mode": privacy_mode,
321
+ }
322
+
323
+
324
+ @mcp.tool()
325
+ def init_usage_pricing_config(force: bool = False) -> dict[str, Any]:
326
+ """Write a local pricing template for optional cost estimates."""
327
+
328
+ output = write_pricing_template(DEFAULT_PRICING_PATH, force=force)
329
+ return {
330
+ "schema": "codex-usage-tracker-init-pricing-v1",
331
+ "pricing_path": str(output),
332
+ "created": True,
333
+ }
334
+
335
+
336
+ @mcp.tool()
337
+ def init_usage_allowance_config(force: bool = False) -> dict[str, Any]:
338
+ """Write a local template for optional Codex allowance windows."""
339
+
340
+ output = write_allowance_template(DEFAULT_ALLOWANCE_PATH, force=force)
341
+ return {
342
+ "schema": "codex-usage-tracker-init-allowance-v1",
343
+ "allowance_path": str(output),
344
+ "created": True,
345
+ }
346
+
347
+
348
+ @mcp.tool()
349
+ def update_usage_pricing_config(
350
+ tier: str = "standard", include_estimates: bool = True
351
+ ) -> dict[str, Any]:
352
+ """Fetch OpenAI-published text-token pricing into the local pricing config."""
353
+
354
+ result = update_pricing_from_openai_docs(
355
+ DEFAULT_PRICING_PATH,
356
+ tier=tier,
357
+ include_estimates=include_estimates,
358
+ )
359
+ return {
360
+ "schema": "codex-usage-tracker-update-pricing-v1",
361
+ "pricing_path": str(result.path),
362
+ "source_url": result.source_url,
363
+ "tier": result.tier,
364
+ "fetched_at": result.fetched_at,
365
+ "model_count": result.model_count,
366
+ "estimated_model_count": result.estimated_model_count,
367
+ "backup_path": str(result.backup_path) if result.backup_path else None,
368
+ }
369
+
370
+ if __name__ == "__main__":
371
+ mcp.run()
@@ -0,0 +1,92 @@
1
+ """Typed records for aggregate Codex usage data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SessionInfo:
10
+ """Metadata from Codex's session index."""
11
+
12
+ session_id: str
13
+ thread_name: str | None
14
+ updated_at: str | None
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class UsageEvent:
19
+ """One aggregate token-count event from a Codex session log."""
20
+
21
+ record_id: str
22
+ session_id: str
23
+ thread_name: str | None
24
+ session_updated_at: str | None
25
+ event_timestamp: str
26
+ source_file: str
27
+ line_number: int
28
+ turn_id: str | None
29
+ turn_timestamp: str | None
30
+ cwd: str | None
31
+ model: str | None
32
+ effort: str | None
33
+ current_date: str | None
34
+ timezone: str | None
35
+ thread_source: str | None
36
+ subagent_type: str | None
37
+ agent_role: str | None
38
+ agent_nickname: str | None
39
+ parent_session_id: str | None
40
+ parent_thread_name: str | None
41
+ parent_session_updated_at: str | None
42
+ model_context_window: int | None
43
+ input_tokens: int
44
+ cached_input_tokens: int
45
+ output_tokens: int
46
+ reasoning_output_tokens: int
47
+ total_tokens: int
48
+ cumulative_input_tokens: int
49
+ cumulative_cached_input_tokens: int
50
+ cumulative_output_tokens: int
51
+ cumulative_reasoning_output_tokens: int
52
+ cumulative_total_tokens: int
53
+
54
+ @property
55
+ def uncached_input_tokens(self) -> int:
56
+ return max(self.input_tokens - self.cached_input_tokens, 0)
57
+
58
+ @property
59
+ def cache_ratio(self) -> float:
60
+ if self.input_tokens <= 0:
61
+ return 0.0
62
+ return self.cached_input_tokens / self.input_tokens
63
+
64
+ @property
65
+ def reasoning_output_ratio(self) -> float:
66
+ if self.output_tokens <= 0:
67
+ return 0.0
68
+ return self.reasoning_output_tokens / self.output_tokens
69
+
70
+ @property
71
+ def context_window_percent(self) -> float:
72
+ if not self.model_context_window:
73
+ return 0.0
74
+ return self.input_tokens / self.model_context_window
75
+
76
+ def to_row(self) -> dict[str, object]:
77
+ row = asdict(self)
78
+ row["uncached_input_tokens"] = self.uncached_input_tokens
79
+ row["cache_ratio"] = self.cache_ratio
80
+ row["reasoning_output_ratio"] = self.reasoning_output_ratio
81
+ row["context_window_percent"] = self.context_window_percent
82
+ return row
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class RefreshResult:
87
+ scanned_files: int
88
+ parsed_events: int
89
+ inserted_or_updated_events: int
90
+ db_path: str
91
+ skipped_events: int = 0
92
+ parser_diagnostics: dict[str, int] = field(default_factory=dict)