codex-stats 1.6.2__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codex_stats-1.6.2/src/codex_stats.egg-info → codex_stats-1.7.0}/PKG-INFO +42 -14
- {codex_stats-1.6.2 → codex_stats-1.7.0}/README.md +41 -13
- {codex_stats-1.6.2 → codex_stats-1.7.0}/pyproject.toml +1 -1
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/__init__.py +1 -1
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/cli.py +122 -20
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/config.py +20 -1
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/display.py +335 -60
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/ingest.py +1 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/metrics.py +63 -1
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/models.py +72 -3
- codex_stats-1.7.0/src/codex_stats/sources.py +440 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/transfer.py +3 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0/src/codex_stats.egg-info}/PKG-INFO +42 -14
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats.egg-info/SOURCES.txt +1 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/tests/test_cli.py +6 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/tests/test_config.py +21 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/tests/test_metrics.py +168 -6
- {codex_stats-1.6.2 → codex_stats-1.7.0}/LICENSE +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/setup.cfg +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/__main__.py +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/completions.py +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/otel.py +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats/watch_state.py +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats.egg-info/dependency_links.txt +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats.egg-info/entry_points.txt +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats.egg-info/requires.txt +0 -0
- {codex_stats-1.6.2 → codex_stats-1.7.0}/src/codex_stats.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-stats
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Local AI usage observability CLI for Codex sessions.
|
|
5
5
|
Author: Vivek
|
|
6
6
|
License: MIT
|
|
@@ -27,18 +27,36 @@ Dynamic: license-file
|
|
|
27
27
|
|
|
28
28
|
# codex-stats
|
|
29
29
|
|
|
30
|
-
`codex-stats` is a local analytics tool for
|
|
30
|
+
`codex-stats` is a local analytics tool for coding agents.
|
|
31
31
|
|
|
32
|
-
It reads
|
|
32
|
+
It reads local session data from every coding assistant installed on the machine and surfaces:
|
|
33
33
|
|
|
34
|
-
- a browser dashboard with
|
|
34
|
+
- a browser dashboard with an **Overview** tab for every tool combined, plus one tab per tool (Codex, OpenCode, Claude Code, Hermes)
|
|
35
|
+
- day, week, month, and all-time windows inside every tab
|
|
35
36
|
- model and project breakdowns
|
|
36
37
|
- recent session history
|
|
37
|
-
- estimated token-based cost
|
|
38
|
+
- estimated token-based cost (or each tool's own recorded cost when available)
|
|
39
|
+
- per-tool cost overrides and a stacked per-tool token trend on the Overview
|
|
38
40
|
- anomaly-aware usage insights and recommendations
|
|
39
41
|
- export for cross-device snapshots
|
|
40
42
|
- shareable JPG cards and browser PDF export from the dashboard
|
|
41
43
|
|
|
44
|
+
## Data Sources
|
|
45
|
+
|
|
46
|
+
Each source is detected automatically and skipped when no data is present:
|
|
47
|
+
|
|
48
|
+
| Tool | Location | Notes |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| Codex | `~/.codex` | `state_5.sqlite` + rollout JSONL files |
|
|
51
|
+
| OpenCode | `~/.local/share/opencode/opencode.db` | recorded cost + tokens per session |
|
|
52
|
+
| Claude Code | `~/.claude/projects/**/*.jsonl` | per-project transcripts |
|
|
53
|
+
| Hermes | `~/.hermes/state.db` | recorded cost + tokens per session |
|
|
54
|
+
|
|
55
|
+
Use these environment variables to point at non-default locations (also used for test isolation):
|
|
56
|
+
|
|
57
|
+
- `CODEX_HOME` (Codex), `CODEX_STATS_OPENCODE_HOME` (OpenCode),
|
|
58
|
+
`CODEX_STATS_CLAUDE_PROJECTS_DIR` (Claude), `CODEX_STATS_HERMES_HOME` (Hermes)
|
|
59
|
+
|
|
42
60
|
## Install
|
|
43
61
|
|
|
44
62
|
```bash
|
|
@@ -59,6 +77,8 @@ python3 -m pip install codex-stats
|
|
|
59
77
|
Write the dashboard HTML to a fixed path.
|
|
60
78
|
- `codex-stats --output codex-stats-dashboard.html --no-open`
|
|
61
79
|
Write the dashboard HTML without opening the browser.
|
|
80
|
+
- `codex-stats --source codex --source opencode`
|
|
81
|
+
Restrict the dashboard to the given tool scopes (repeat the flag to pick multiple).
|
|
62
82
|
- `codex-stats export codex-stats-export.json`
|
|
63
83
|
Export normalized local stats to JSON.
|
|
64
84
|
- `codex-stats export codex-stats-export.json --since 30d`
|
|
@@ -66,23 +86,31 @@ python3 -m pip install codex-stats
|
|
|
66
86
|
|
|
67
87
|
Inside the dashboard, use the action bar to:
|
|
68
88
|
|
|
69
|
-
- switch
|
|
70
|
-
-
|
|
89
|
+
- switch tools with the **Overview / Codex / OpenCode / Claude Code / Hermes** tab row
|
|
90
|
+
- switch between `Day`, `Week`, `Month`, and `All Time` inside the active tool
|
|
91
|
+
- print the active tool and window to PDF
|
|
71
92
|
- download shareable JPG cards for summary, cost, focus, and project share
|
|
72
93
|
|
|
73
94
|
## How It Works
|
|
74
95
|
|
|
75
|
-
`codex-stats` does not proxy or intercept
|
|
96
|
+
`codex-stats` does not proxy or intercept API traffic. It reads local artifacts:
|
|
97
|
+
Codex `state_5.sqlite` and rollout files, the OpenCode database, Claude Code project
|
|
98
|
+
transcripts, and the Hermes database, then normalizes everything into one session model.
|
|
76
99
|
|
|
77
|
-
|
|
100
|
+
## Notes
|
|
78
101
|
|
|
79
|
-
-
|
|
80
|
-
|
|
102
|
+
- Costs are estimates by default. When a tool records its own cost (OpenCode, Hermes),
|
|
103
|
+
that value wins for the session. Per-tool rates can be set in `~/.config/codex-stats/config.toml`:
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
```toml
|
|
106
|
+
[pricing.source_usd_per_1k_tokens]
|
|
107
|
+
codex = 0.01
|
|
108
|
+
opencode = 0.01
|
|
109
|
+
claude = 0.015
|
|
110
|
+
hermes = 0.008
|
|
111
|
+
```
|
|
83
112
|
|
|
84
|
-
-
|
|
85
|
-
- Output depends on local Codex file formats remaining compatible.
|
|
113
|
+
- Output depends on local file formats remaining compatible.
|
|
86
114
|
- `export --since Nd` limits snapshots to a rolling window before sharing.
|
|
87
115
|
|
|
88
116
|
## Roadmap
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
# codex-stats
|
|
2
2
|
|
|
3
|
-
`codex-stats` is a local analytics tool for
|
|
3
|
+
`codex-stats` is a local analytics tool for coding agents.
|
|
4
4
|
|
|
5
|
-
It reads
|
|
5
|
+
It reads local session data from every coding assistant installed on the machine and surfaces:
|
|
6
6
|
|
|
7
|
-
- a browser dashboard with
|
|
7
|
+
- a browser dashboard with an **Overview** tab for every tool combined, plus one tab per tool (Codex, OpenCode, Claude Code, Hermes)
|
|
8
|
+
- day, week, month, and all-time windows inside every tab
|
|
8
9
|
- model and project breakdowns
|
|
9
10
|
- recent session history
|
|
10
|
-
- estimated token-based cost
|
|
11
|
+
- estimated token-based cost (or each tool's own recorded cost when available)
|
|
12
|
+
- per-tool cost overrides and a stacked per-tool token trend on the Overview
|
|
11
13
|
- anomaly-aware usage insights and recommendations
|
|
12
14
|
- export for cross-device snapshots
|
|
13
15
|
- shareable JPG cards and browser PDF export from the dashboard
|
|
14
16
|
|
|
17
|
+
## Data Sources
|
|
18
|
+
|
|
19
|
+
Each source is detected automatically and skipped when no data is present:
|
|
20
|
+
|
|
21
|
+
| Tool | Location | Notes |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Codex | `~/.codex` | `state_5.sqlite` + rollout JSONL files |
|
|
24
|
+
| OpenCode | `~/.local/share/opencode/opencode.db` | recorded cost + tokens per session |
|
|
25
|
+
| Claude Code | `~/.claude/projects/**/*.jsonl` | per-project transcripts |
|
|
26
|
+
| Hermes | `~/.hermes/state.db` | recorded cost + tokens per session |
|
|
27
|
+
|
|
28
|
+
Use these environment variables to point at non-default locations (also used for test isolation):
|
|
29
|
+
|
|
30
|
+
- `CODEX_HOME` (Codex), `CODEX_STATS_OPENCODE_HOME` (OpenCode),
|
|
31
|
+
`CODEX_STATS_CLAUDE_PROJECTS_DIR` (Claude), `CODEX_STATS_HERMES_HOME` (Hermes)
|
|
32
|
+
|
|
15
33
|
## Install
|
|
16
34
|
|
|
17
35
|
```bash
|
|
@@ -32,6 +50,8 @@ python3 -m pip install codex-stats
|
|
|
32
50
|
Write the dashboard HTML to a fixed path.
|
|
33
51
|
- `codex-stats --output codex-stats-dashboard.html --no-open`
|
|
34
52
|
Write the dashboard HTML without opening the browser.
|
|
53
|
+
- `codex-stats --source codex --source opencode`
|
|
54
|
+
Restrict the dashboard to the given tool scopes (repeat the flag to pick multiple).
|
|
35
55
|
- `codex-stats export codex-stats-export.json`
|
|
36
56
|
Export normalized local stats to JSON.
|
|
37
57
|
- `codex-stats export codex-stats-export.json --since 30d`
|
|
@@ -39,23 +59,31 @@ python3 -m pip install codex-stats
|
|
|
39
59
|
|
|
40
60
|
Inside the dashboard, use the action bar to:
|
|
41
61
|
|
|
42
|
-
- switch
|
|
43
|
-
-
|
|
62
|
+
- switch tools with the **Overview / Codex / OpenCode / Claude Code / Hermes** tab row
|
|
63
|
+
- switch between `Day`, `Week`, `Month`, and `All Time` inside the active tool
|
|
64
|
+
- print the active tool and window to PDF
|
|
44
65
|
- download shareable JPG cards for summary, cost, focus, and project share
|
|
45
66
|
|
|
46
67
|
## How It Works
|
|
47
68
|
|
|
48
|
-
`codex-stats` does not proxy or intercept
|
|
69
|
+
`codex-stats` does not proxy or intercept API traffic. It reads local artifacts:
|
|
70
|
+
Codex `state_5.sqlite` and rollout files, the OpenCode database, Claude Code project
|
|
71
|
+
transcripts, and the Hermes database, then normalizes everything into one session model.
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
## Notes
|
|
51
74
|
|
|
52
|
-
-
|
|
53
|
-
|
|
75
|
+
- Costs are estimates by default. When a tool records its own cost (OpenCode, Hermes),
|
|
76
|
+
that value wins for the session. Per-tool rates can be set in `~/.config/codex-stats/config.toml`:
|
|
54
77
|
|
|
55
|
-
|
|
78
|
+
```toml
|
|
79
|
+
[pricing.source_usd_per_1k_tokens]
|
|
80
|
+
codex = 0.01
|
|
81
|
+
opencode = 0.01
|
|
82
|
+
claude = 0.015
|
|
83
|
+
hermes = 0.008
|
|
84
|
+
```
|
|
56
85
|
|
|
57
|
-
-
|
|
58
|
-
- Output depends on local Codex file formats remaining compatible.
|
|
86
|
+
- Output depends on local file formats remaining compatible.
|
|
59
87
|
- `export --since Nd` limits snapshots to a rolling window before sharing.
|
|
60
88
|
|
|
61
89
|
## Roadmap
|
|
@@ -21,12 +21,19 @@ from .metrics import (
|
|
|
21
21
|
summarize_insights_from_details,
|
|
22
22
|
summarize_project_drilldowns_from_details,
|
|
23
23
|
summarize_projects_from_details,
|
|
24
|
+
summarize_source_breakdown_from_details,
|
|
25
|
+
summarize_source_daily_from_details,
|
|
24
26
|
summarize_takeaways,
|
|
25
27
|
summarize_top_sessions_from_details,
|
|
26
28
|
summarize_work_rhythm,
|
|
27
29
|
)
|
|
28
|
-
from .
|
|
29
|
-
|
|
30
|
+
from .models import (
|
|
31
|
+
DashboardData,
|
|
32
|
+
DashboardScope,
|
|
33
|
+
DashboardWindow,
|
|
34
|
+
SessionDetails,
|
|
35
|
+
)
|
|
36
|
+
from .sources import Source, iter_sources
|
|
30
37
|
from .transfer import write_export
|
|
31
38
|
|
|
32
39
|
|
|
@@ -41,6 +48,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
41
48
|
help="Write dashboard HTML to a file instead of a temp file.",
|
|
42
49
|
)
|
|
43
50
|
parser.add_argument("--no-open", action="store_true", help="Write the dashboard without opening the browser.")
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--source",
|
|
53
|
+
action="append",
|
|
54
|
+
dest="sources",
|
|
55
|
+
metavar="SOURCE",
|
|
56
|
+
help="Only include the given tool scope (codex, opencode, claude, hermes). Repeatable: --source codex --source opencode.",
|
|
57
|
+
)
|
|
44
58
|
subparsers = parser.add_subparsers(dest="command")
|
|
45
59
|
|
|
46
60
|
export_parser = subparsers.add_parser("export", help="Export normalized local stats to JSON.")
|
|
@@ -63,7 +77,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
63
77
|
parser.print_help()
|
|
64
78
|
return 1
|
|
65
79
|
|
|
66
|
-
dashboard = _build_dashboard(paths)
|
|
80
|
+
dashboard = _build_dashboard(paths, sources=args.sources)
|
|
67
81
|
output_path = _write_dashboard_output(format_dashboard_html(dashboard), args.dashboard_output)
|
|
68
82
|
print(f"Wrote dashboard to {output_path}")
|
|
69
83
|
if not args.no_open:
|
|
@@ -72,62 +86,118 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
72
86
|
return 0
|
|
73
87
|
|
|
74
88
|
|
|
75
|
-
def _build_dashboard(paths: Paths, now: datetime | None = None) -> DashboardData:
|
|
89
|
+
def _build_dashboard(paths: Paths, now: datetime | None = None, sources: list[str] | None = None) -> DashboardData:
|
|
76
90
|
current_time = now or datetime.now().astimezone()
|
|
77
91
|
pricing = load_pricing_config(paths)
|
|
78
|
-
|
|
92
|
+
all_sources = iter_sources(paths)
|
|
93
|
+
if sources:
|
|
94
|
+
requested = set(sources)
|
|
95
|
+
missing = sorted(requested - {source.key for source in all_sources})
|
|
96
|
+
if missing:
|
|
97
|
+
known = ", ".join(sorted(source.key for source in all_sources)) or "none"
|
|
98
|
+
raise ValueError(f"Unknown source(s): {', '.join(missing)}. Known sources: {known}")
|
|
99
|
+
selected_sources = [source for source in all_sources if source.key in requested]
|
|
100
|
+
else:
|
|
101
|
+
selected_sources = all_sources
|
|
102
|
+
source_details = {source.key: source.ingest() for source in selected_sources}
|
|
103
|
+
all_details = [detail for source in selected_sources for detail in source_details[source.key]]
|
|
104
|
+
|
|
105
|
+
overview = _build_scope(
|
|
106
|
+
key="overview",
|
|
107
|
+
label="Overview",
|
|
108
|
+
description="Every tool combined. Start here to see total activity, spend, and where the work happened.",
|
|
109
|
+
source="all",
|
|
110
|
+
details=all_details,
|
|
111
|
+
pricing=pricing,
|
|
112
|
+
now=current_time,
|
|
113
|
+
build_tool_breakdown=True,
|
|
114
|
+
)
|
|
115
|
+
tool_scopes = [
|
|
116
|
+
_build_scope(
|
|
117
|
+
key=source.key,
|
|
118
|
+
label=source.label,
|
|
119
|
+
description=source.description,
|
|
120
|
+
source=source.key,
|
|
121
|
+
details=source_details[source.key],
|
|
122
|
+
pricing=pricing,
|
|
123
|
+
now=current_time,
|
|
124
|
+
)
|
|
125
|
+
for source in selected_sources
|
|
126
|
+
]
|
|
127
|
+
return DashboardData(generated_at=current_time, scopes=[overview, *tool_scopes])
|
|
79
128
|
|
|
80
|
-
today_details = _details_for_last_days(all_details, 1, current_time)
|
|
81
|
-
week_details = _details_for_last_days(all_details, 7, current_time)
|
|
82
|
-
month_details = _details_for_last_days(all_details, 30, current_time)
|
|
83
129
|
|
|
130
|
+
def _build_scope(
|
|
131
|
+
*,
|
|
132
|
+
key: str,
|
|
133
|
+
label: str,
|
|
134
|
+
description: str,
|
|
135
|
+
source: str,
|
|
136
|
+
details: list[SessionDetails],
|
|
137
|
+
pricing: PricingConfig,
|
|
138
|
+
now: datetime,
|
|
139
|
+
build_tool_breakdown: bool = False,
|
|
140
|
+
) -> DashboardScope:
|
|
141
|
+
today_details = _details_for_last_days(details, 1, now)
|
|
142
|
+
week_details = _details_for_last_days(details, 7, now)
|
|
143
|
+
month_details = _details_for_last_days(details, 30, now)
|
|
84
144
|
windows = [
|
|
85
145
|
_build_window(
|
|
86
146
|
key="day",
|
|
87
147
|
label="Day",
|
|
88
148
|
description="Today’s usage with a direct comparison to yesterday.",
|
|
89
149
|
current_details=today_details,
|
|
90
|
-
previous_details=_details_for_previous_window(
|
|
150
|
+
previous_details=_details_for_previous_window(details, 1, now),
|
|
91
151
|
current_label="today",
|
|
92
152
|
previous_label="yesterday",
|
|
93
153
|
trend_days=1,
|
|
94
|
-
all_details=
|
|
154
|
+
all_details=details,
|
|
95
155
|
pricing=pricing,
|
|
96
|
-
now=
|
|
156
|
+
now=now,
|
|
157
|
+
build_tool_breakdown=build_tool_breakdown,
|
|
97
158
|
),
|
|
98
159
|
_build_window(
|
|
99
160
|
key="week",
|
|
100
161
|
label="Week",
|
|
101
162
|
description="Rolling 7-day totals, recent trend, and the busiest sessions this week.",
|
|
102
163
|
current_details=week_details,
|
|
103
|
-
previous_details=_details_for_previous_window(
|
|
164
|
+
previous_details=_details_for_previous_window(details, 7, now),
|
|
104
165
|
current_label="last 7 days",
|
|
105
166
|
previous_label="previous 7 days",
|
|
106
167
|
trend_days=7,
|
|
107
|
-
all_details=
|
|
168
|
+
all_details=details,
|
|
108
169
|
pricing=pricing,
|
|
109
|
-
now=
|
|
170
|
+
now=now,
|
|
171
|
+
build_tool_breakdown=build_tool_breakdown,
|
|
110
172
|
),
|
|
111
173
|
_build_window(
|
|
112
174
|
key="month",
|
|
113
175
|
label="Month",
|
|
114
176
|
description="Rolling 30-day totals with project concentration and cost pressure.",
|
|
115
177
|
current_details=month_details,
|
|
116
|
-
previous_details=_details_for_previous_window(
|
|
178
|
+
previous_details=_details_for_previous_window(details, 30, now),
|
|
117
179
|
current_label="last 30 days",
|
|
118
180
|
previous_label="previous 30 days",
|
|
119
181
|
trend_days=30,
|
|
120
|
-
all_details=
|
|
182
|
+
all_details=details,
|
|
121
183
|
pricing=pricing,
|
|
122
|
-
now=
|
|
184
|
+
now=now,
|
|
185
|
+
build_tool_breakdown=build_tool_breakdown,
|
|
123
186
|
),
|
|
124
187
|
_build_all_time_window(
|
|
125
|
-
all_details=
|
|
188
|
+
all_details=details,
|
|
126
189
|
pricing=pricing,
|
|
127
|
-
now=
|
|
190
|
+
now=now,
|
|
191
|
+
build_tool_breakdown=build_tool_breakdown,
|
|
128
192
|
),
|
|
129
193
|
]
|
|
130
|
-
return
|
|
194
|
+
return DashboardScope(
|
|
195
|
+
key=key,
|
|
196
|
+
label=label,
|
|
197
|
+
description=description,
|
|
198
|
+
source=source,
|
|
199
|
+
windows=windows,
|
|
200
|
+
)
|
|
131
201
|
|
|
132
202
|
|
|
133
203
|
def _build_window(
|
|
@@ -143,6 +213,7 @@ def _build_window(
|
|
|
143
213
|
all_details: list[SessionDetails],
|
|
144
214
|
pricing: PricingConfig,
|
|
145
215
|
now: datetime,
|
|
216
|
+
build_tool_breakdown: bool = False,
|
|
146
217
|
) -> DashboardWindow:
|
|
147
218
|
summary = summarize_details(current_label, current_details, pricing)
|
|
148
219
|
comparison = summarize_compare_from_details(
|
|
@@ -189,6 +260,21 @@ def _build_window(
|
|
|
189
260
|
pricing=pricing,
|
|
190
261
|
limit=5,
|
|
191
262
|
),
|
|
263
|
+
tool_breakdown=(
|
|
264
|
+
summarize_source_breakdown_from_details(current_details, pricing)
|
|
265
|
+
if build_tool_breakdown
|
|
266
|
+
else None
|
|
267
|
+
),
|
|
268
|
+
tool_daily_points=(
|
|
269
|
+
summarize_source_daily_from_details(
|
|
270
|
+
current_details,
|
|
271
|
+
days=max(trend_days, 1),
|
|
272
|
+
now=now,
|
|
273
|
+
pricing=pricing,
|
|
274
|
+
)
|
|
275
|
+
if build_tool_breakdown
|
|
276
|
+
else None
|
|
277
|
+
),
|
|
192
278
|
)
|
|
193
279
|
|
|
194
280
|
|
|
@@ -197,6 +283,7 @@ def _build_all_time_window(
|
|
|
197
283
|
all_details: list[SessionDetails],
|
|
198
284
|
pricing: PricingConfig,
|
|
199
285
|
now: datetime,
|
|
286
|
+
build_tool_breakdown: bool = False,
|
|
200
287
|
) -> DashboardWindow:
|
|
201
288
|
summary = summarize_details("all time", all_details, pricing)
|
|
202
289
|
recent_details = _details_for_last_days(all_details, 30, now)
|
|
@@ -244,6 +331,21 @@ def _build_all_time_window(
|
|
|
244
331
|
pricing=pricing,
|
|
245
332
|
limit=5,
|
|
246
333
|
),
|
|
334
|
+
tool_breakdown=(
|
|
335
|
+
summarize_source_breakdown_from_details(all_details, pricing)
|
|
336
|
+
if build_tool_breakdown
|
|
337
|
+
else None
|
|
338
|
+
),
|
|
339
|
+
tool_daily_points=(
|
|
340
|
+
summarize_source_daily_from_details(
|
|
341
|
+
all_details,
|
|
342
|
+
days=trend_days,
|
|
343
|
+
now=now,
|
|
344
|
+
pricing=pricing,
|
|
345
|
+
)
|
|
346
|
+
if build_tool_breakdown
|
|
347
|
+
else None
|
|
348
|
+
),
|
|
247
349
|
)
|
|
248
350
|
|
|
249
351
|
|
|
@@ -15,6 +15,13 @@ default_usd_per_1k_tokens = 0.01
|
|
|
15
15
|
# gpt-5.4 = 0.02
|
|
16
16
|
# gpt-5-mini = 0.005
|
|
17
17
|
|
|
18
|
+
[pricing.source_usd_per_1k_tokens]
|
|
19
|
+
# Per-tool overrides when a tool does not record its own cost.
|
|
20
|
+
# codex = 0.01
|
|
21
|
+
# opencode = 0.01
|
|
22
|
+
# claude = 0.015
|
|
23
|
+
# hermes = 0.008
|
|
24
|
+
|
|
18
25
|
[display]
|
|
19
26
|
color = "auto"
|
|
20
27
|
history_limit = 10
|
|
@@ -51,12 +58,18 @@ class Paths:
|
|
|
51
58
|
class PricingConfig:
|
|
52
59
|
default_usd_per_1k_tokens: float = 0.01
|
|
53
60
|
model_rates: dict[str, float] | None = None
|
|
61
|
+
source_rates: dict[str, float] | None = None
|
|
54
62
|
|
|
55
63
|
def rate_for_model(self, model: str | None) -> float:
|
|
56
64
|
if model and self.model_rates and model in self.model_rates:
|
|
57
65
|
return self.model_rates[model]
|
|
58
66
|
return self.default_usd_per_1k_tokens
|
|
59
67
|
|
|
68
|
+
def rate_for_source(self, source: str, model: str | None) -> float:
|
|
69
|
+
if source and self.source_rates and source in self.source_rates:
|
|
70
|
+
return self.source_rates[source]
|
|
71
|
+
return self.rate_for_model(model)
|
|
72
|
+
|
|
60
73
|
|
|
61
74
|
@dataclass(frozen=True)
|
|
62
75
|
class DisplayConfig:
|
|
@@ -79,6 +92,7 @@ def load_config(paths: Paths) -> AppConfig:
|
|
|
79
92
|
pricing = payload.get("pricing", {})
|
|
80
93
|
default_rate = float(pricing.get("default_usd_per_1k_tokens", 0.01))
|
|
81
94
|
model_rates = _flatten_model_rates(pricing.get("model_usd_per_1k_tokens", {}))
|
|
95
|
+
source_rates = {str(k): float(v) for k, v in pricing.get("source_usd_per_1k_tokens", {}).items()}
|
|
82
96
|
display = payload.get("display", {})
|
|
83
97
|
color = str(display.get("color", "auto"))
|
|
84
98
|
if color not in {"auto", "always", "never"}:
|
|
@@ -90,7 +104,11 @@ def load_config(paths: Paths) -> AppConfig:
|
|
|
90
104
|
if compare_days <= 0:
|
|
91
105
|
raise ValueError("display.compare_days must be greater than 0")
|
|
92
106
|
return AppConfig(
|
|
93
|
-
pricing=PricingConfig(
|
|
107
|
+
pricing=PricingConfig(
|
|
108
|
+
default_usd_per_1k_tokens=default_rate,
|
|
109
|
+
model_rates=model_rates,
|
|
110
|
+
source_rates=source_rates,
|
|
111
|
+
),
|
|
94
112
|
display=DisplayConfig(color=color, history_limit=history_limit, compare_days=compare_days),
|
|
95
113
|
)
|
|
96
114
|
|
|
@@ -110,6 +128,7 @@ def load_config_view(paths: Paths) -> ConfigView:
|
|
|
110
128
|
exists=paths.config_file.exists(),
|
|
111
129
|
pricing_default_usd_per_1k_tokens=app_config.pricing.default_usd_per_1k_tokens,
|
|
112
130
|
pricing_model_overrides=app_config.pricing.model_rates or {},
|
|
131
|
+
pricing_source_overrides=app_config.pricing.source_rates or {},
|
|
113
132
|
display=DisplayConfigView(
|
|
114
133
|
color=app_config.display.color,
|
|
115
134
|
history_limit=app_config.display.history_limit,
|