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,637 @@
|
|
|
1
|
+
"""Shared report application services for CLI and MCP surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import date, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from codex_usage_tracker.allowance import (
|
|
11
|
+
annotate_rows_with_allowance,
|
|
12
|
+
load_allowance_config,
|
|
13
|
+
)
|
|
14
|
+
from codex_usage_tracker.formatting import (
|
|
15
|
+
format_calls,
|
|
16
|
+
format_pricing_coverage,
|
|
17
|
+
format_recommendations,
|
|
18
|
+
format_summary,
|
|
19
|
+
)
|
|
20
|
+
from codex_usage_tracker.paths import DEFAULT_PROJECTS_PATH
|
|
21
|
+
from codex_usage_tracker.pricing import (
|
|
22
|
+
PricingConfig,
|
|
23
|
+
annotate_rows_with_efficiency,
|
|
24
|
+
load_pricing_config,
|
|
25
|
+
summarize_pricing_coverage,
|
|
26
|
+
)
|
|
27
|
+
from codex_usage_tracker.projects import (
|
|
28
|
+
annotate_rows_with_project_identity,
|
|
29
|
+
apply_project_privacy_to_rows,
|
|
30
|
+
apply_project_privacy_to_summary_rows,
|
|
31
|
+
load_project_config,
|
|
32
|
+
validate_privacy_mode,
|
|
33
|
+
)
|
|
34
|
+
from codex_usage_tracker.recommendations import annotate_rows_with_recommendations
|
|
35
|
+
from codex_usage_tracker.store import (
|
|
36
|
+
query_dashboard_events,
|
|
37
|
+
query_most_expensive_calls,
|
|
38
|
+
query_summary,
|
|
39
|
+
)
|
|
40
|
+
from codex_usage_tracker.threads import annotate_thread_attachments
|
|
41
|
+
|
|
42
|
+
SUMMARY_GROUP_BY_CHOICES = (
|
|
43
|
+
"date",
|
|
44
|
+
"model",
|
|
45
|
+
"effort",
|
|
46
|
+
"cwd",
|
|
47
|
+
"project",
|
|
48
|
+
"project_tag",
|
|
49
|
+
"thread",
|
|
50
|
+
"session",
|
|
51
|
+
"thread_source",
|
|
52
|
+
"subagent_type",
|
|
53
|
+
"agent_role",
|
|
54
|
+
"parent_session",
|
|
55
|
+
"parent_thread",
|
|
56
|
+
)
|
|
57
|
+
SUMMARY_PRESET_CHOICES = (
|
|
58
|
+
"today",
|
|
59
|
+
"last-7-days",
|
|
60
|
+
"by-model",
|
|
61
|
+
"by-cwd",
|
|
62
|
+
"by-project",
|
|
63
|
+
"by-project-tag",
|
|
64
|
+
"by-thread",
|
|
65
|
+
"by-subagent-role",
|
|
66
|
+
"by-subagent-type",
|
|
67
|
+
"expensive",
|
|
68
|
+
)
|
|
69
|
+
EXPENSIVE_PRESET_CHOICES = ("today", "last-7-days")
|
|
70
|
+
QUERY_PRICING_STATUS_CHOICES = ("priced", "estimated", "unpriced")
|
|
71
|
+
QUERY_CREDIT_CONFIDENCE_CHOICES = ("exact", "estimated", "unpriced", "user_override")
|
|
72
|
+
|
|
73
|
+
_SUMMARY_PRESET_GROUPS = {
|
|
74
|
+
"by-model": "model",
|
|
75
|
+
"by-cwd": "cwd",
|
|
76
|
+
"by-project": "project",
|
|
77
|
+
"by-project-tag": "project_tag",
|
|
78
|
+
"by-thread": "thread",
|
|
79
|
+
"by-subagent-role": "agent_role",
|
|
80
|
+
"by-subagent-type": "subagent_type",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class SummaryReport:
|
|
86
|
+
"""Resolved aggregate usage summary for one display surface."""
|
|
87
|
+
|
|
88
|
+
rows: list[dict[str, Any]]
|
|
89
|
+
group_by: str
|
|
90
|
+
is_expensive: bool = False
|
|
91
|
+
privacy_mode: str = "normal"
|
|
92
|
+
|
|
93
|
+
def render(self) -> str:
|
|
94
|
+
if self.is_expensive:
|
|
95
|
+
return format_calls(self.rows)
|
|
96
|
+
return format_summary(self.rows, self.group_by)
|
|
97
|
+
|
|
98
|
+
def payload(self) -> dict[str, Any]:
|
|
99
|
+
return {
|
|
100
|
+
"schema": "codex-usage-tracker-summary-v1",
|
|
101
|
+
"group_by": self.group_by,
|
|
102
|
+
"is_expensive": self.is_expensive,
|
|
103
|
+
"privacy_mode": self.privacy_mode,
|
|
104
|
+
"row_count": len(self.rows),
|
|
105
|
+
"rows": self.rows,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class PricingCoverageReport:
|
|
111
|
+
"""Resolved pricing coverage report."""
|
|
112
|
+
|
|
113
|
+
payload: dict[str, Any]
|
|
114
|
+
|
|
115
|
+
def render(self, limit: int = 20) -> str:
|
|
116
|
+
return format_pricing_coverage(self.payload, limit=limit)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class QueryReport:
|
|
121
|
+
"""Stable machine-readable aggregate usage query result."""
|
|
122
|
+
|
|
123
|
+
payload: dict[str, Any]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class RecommendationsReport:
|
|
128
|
+
"""Stable recommendation ranking for aggregate usage rows and threads."""
|
|
129
|
+
|
|
130
|
+
payload: dict[str, Any]
|
|
131
|
+
|
|
132
|
+
def render(self) -> str:
|
|
133
|
+
return format_recommendations(self.payload)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_summary_options(
|
|
137
|
+
group_by: str, preset: str | None, since: str | None
|
|
138
|
+
) -> tuple[str, str | None]:
|
|
139
|
+
"""Resolve summary presets into a group and since filter."""
|
|
140
|
+
|
|
141
|
+
return _SUMMARY_PRESET_GROUPS.get(preset or "", group_by), resolve_since(preset, since)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_since(preset: str | None, since: str | None) -> str | None:
|
|
145
|
+
"""Resolve date presets into an ISO date string."""
|
|
146
|
+
|
|
147
|
+
if since:
|
|
148
|
+
return since
|
|
149
|
+
if preset == "today":
|
|
150
|
+
return date.today().isoformat()
|
|
151
|
+
if preset == "last-7-days":
|
|
152
|
+
return (date.today() - timedelta(days=6)).isoformat()
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_summary_report(
|
|
157
|
+
*,
|
|
158
|
+
db_path: Path,
|
|
159
|
+
pricing_path: Path,
|
|
160
|
+
group_by: str = "thread",
|
|
161
|
+
limit: int = 20,
|
|
162
|
+
preset: str | None = None,
|
|
163
|
+
since: str | None = None,
|
|
164
|
+
projects_path: Path = DEFAULT_PROJECTS_PATH,
|
|
165
|
+
privacy_mode: str = "normal",
|
|
166
|
+
) -> SummaryReport:
|
|
167
|
+
"""Build a usage summary or expensive-call preset from aggregate rows."""
|
|
168
|
+
|
|
169
|
+
privacy_mode = validate_privacy_mode(privacy_mode)
|
|
170
|
+
resolved_group_by, since_filter = resolve_summary_options(group_by, preset, since)
|
|
171
|
+
pricing = load_pricing_config(pricing_path)
|
|
172
|
+
if preset == "expensive":
|
|
173
|
+
rows = query_most_expensive_calls(db_path, limit=limit, since=since_filter)
|
|
174
|
+
return SummaryReport(
|
|
175
|
+
rows=apply_project_privacy_to_rows(
|
|
176
|
+
annotate_rows_with_recommendations(
|
|
177
|
+
annotate_rows_with_efficiency(rows, pricing)
|
|
178
|
+
),
|
|
179
|
+
privacy_mode=privacy_mode,
|
|
180
|
+
),
|
|
181
|
+
group_by=resolved_group_by,
|
|
182
|
+
is_expensive=True,
|
|
183
|
+
privacy_mode=privacy_mode,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if resolved_group_by in {"project", "project_tag"}:
|
|
187
|
+
rows = _project_summary_rows(
|
|
188
|
+
db_path=db_path,
|
|
189
|
+
pricing=pricing,
|
|
190
|
+
group_by=resolved_group_by,
|
|
191
|
+
limit=limit,
|
|
192
|
+
since=since_filter,
|
|
193
|
+
projects_path=projects_path,
|
|
194
|
+
privacy_mode=privacy_mode,
|
|
195
|
+
)
|
|
196
|
+
return SummaryReport(rows=rows, group_by=resolved_group_by, privacy_mode=privacy_mode)
|
|
197
|
+
|
|
198
|
+
rows = query_summary(
|
|
199
|
+
db_path,
|
|
200
|
+
group_by=resolved_group_by,
|
|
201
|
+
limit=limit,
|
|
202
|
+
since=since_filter,
|
|
203
|
+
)
|
|
204
|
+
if resolved_group_by == "model":
|
|
205
|
+
rows = annotate_rows_with_efficiency(rows, pricing, model_field="group_key")
|
|
206
|
+
rows = apply_project_privacy_to_summary_rows(
|
|
207
|
+
rows, group_by=resolved_group_by, privacy_mode=privacy_mode
|
|
208
|
+
)
|
|
209
|
+
return SummaryReport(rows=rows, group_by=resolved_group_by, privacy_mode=privacy_mode)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_expensive_calls_report(
|
|
213
|
+
*,
|
|
214
|
+
db_path: Path,
|
|
215
|
+
pricing_path: Path,
|
|
216
|
+
limit: int = 20,
|
|
217
|
+
preset: str | None = None,
|
|
218
|
+
since: str | None = None,
|
|
219
|
+
privacy_mode: str = "normal",
|
|
220
|
+
) -> SummaryReport:
|
|
221
|
+
"""Build a highest-token-call report with pricing efficiency annotations."""
|
|
222
|
+
|
|
223
|
+
privacy_mode = validate_privacy_mode(privacy_mode)
|
|
224
|
+
pricing = load_pricing_config(pricing_path)
|
|
225
|
+
rows = query_most_expensive_calls(
|
|
226
|
+
db_path,
|
|
227
|
+
limit=limit,
|
|
228
|
+
since=resolve_since(preset, since),
|
|
229
|
+
)
|
|
230
|
+
return SummaryReport(
|
|
231
|
+
rows=apply_project_privacy_to_rows(
|
|
232
|
+
annotate_rows_with_recommendations(annotate_rows_with_efficiency(rows, pricing)),
|
|
233
|
+
privacy_mode=privacy_mode,
|
|
234
|
+
),
|
|
235
|
+
group_by="call",
|
|
236
|
+
is_expensive=True,
|
|
237
|
+
privacy_mode=privacy_mode,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def build_pricing_coverage_report(
|
|
242
|
+
*,
|
|
243
|
+
db_path: Path,
|
|
244
|
+
pricing_path: Path,
|
|
245
|
+
limit: int = 1000,
|
|
246
|
+
since: str | None = None,
|
|
247
|
+
pricing: PricingConfig | None = None,
|
|
248
|
+
) -> PricingCoverageReport:
|
|
249
|
+
"""Build pricing coverage data grouped by model."""
|
|
250
|
+
|
|
251
|
+
config = pricing or load_pricing_config(pricing_path)
|
|
252
|
+
rows = query_summary(db_path, group_by="model", limit=limit, since=since)
|
|
253
|
+
return PricingCoverageReport(summarize_pricing_coverage(rows, pricing=config))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def build_query_report(
|
|
257
|
+
*,
|
|
258
|
+
db_path: Path,
|
|
259
|
+
pricing_path: Path,
|
|
260
|
+
allowance_path: Path,
|
|
261
|
+
projects_path: Path = DEFAULT_PROJECTS_PATH,
|
|
262
|
+
since: str | None = None,
|
|
263
|
+
until: str | None = None,
|
|
264
|
+
model: str | None = None,
|
|
265
|
+
effort: str | None = None,
|
|
266
|
+
thread: str | None = None,
|
|
267
|
+
project: str | None = None,
|
|
268
|
+
pricing_status: str | None = None,
|
|
269
|
+
credit_confidence: str | None = None,
|
|
270
|
+
min_tokens: int | None = None,
|
|
271
|
+
min_credits: float | None = None,
|
|
272
|
+
limit: int = 100,
|
|
273
|
+
privacy_mode: str = "normal",
|
|
274
|
+
) -> QueryReport:
|
|
275
|
+
"""Build a stable JSON usage query with aggregate-only annotated rows."""
|
|
276
|
+
|
|
277
|
+
privacy_mode = validate_privacy_mode(privacy_mode)
|
|
278
|
+
if pricing_status and pricing_status not in QUERY_PRICING_STATUS_CHOICES:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"pricing_status must be one of: {', '.join(QUERY_PRICING_STATUS_CHOICES)}"
|
|
281
|
+
)
|
|
282
|
+
if credit_confidence and credit_confidence not in QUERY_CREDIT_CONFIDENCE_CHOICES:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"credit_confidence must be one of: {', '.join(QUERY_CREDIT_CONFIDENCE_CHOICES)}"
|
|
285
|
+
)
|
|
286
|
+
rows = annotate_thread_attachments(
|
|
287
|
+
query_dashboard_events(
|
|
288
|
+
db_path,
|
|
289
|
+
limit=0,
|
|
290
|
+
since=since,
|
|
291
|
+
until=until,
|
|
292
|
+
model=model,
|
|
293
|
+
effort=effort,
|
|
294
|
+
thread=thread,
|
|
295
|
+
min_tokens=min_tokens,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
pricing = load_pricing_config(pricing_path)
|
|
299
|
+
allowance = load_allowance_config(allowance_path)
|
|
300
|
+
rows = annotate_rows_with_allowance(annotate_rows_with_efficiency(rows, pricing), allowance)
|
|
301
|
+
rows = annotate_rows_with_recommendations(rows)
|
|
302
|
+
rows = annotate_rows_with_project_identity(rows, load_project_config(projects_path))
|
|
303
|
+
rows = [
|
|
304
|
+
row
|
|
305
|
+
for row in rows
|
|
306
|
+
if _query_row_matches(
|
|
307
|
+
row,
|
|
308
|
+
until=until,
|
|
309
|
+
model=model,
|
|
310
|
+
effort=effort,
|
|
311
|
+
thread=thread,
|
|
312
|
+
project=project,
|
|
313
|
+
pricing_status=pricing_status,
|
|
314
|
+
credit_confidence=credit_confidence,
|
|
315
|
+
min_tokens=min_tokens,
|
|
316
|
+
min_credits=min_credits,
|
|
317
|
+
)
|
|
318
|
+
]
|
|
319
|
+
rows = apply_project_privacy_to_rows(rows, privacy_mode=privacy_mode)
|
|
320
|
+
normalized_limit = None if limit <= 0 else limit
|
|
321
|
+
limited_rows = rows if normalized_limit is None else rows[:normalized_limit]
|
|
322
|
+
return QueryReport(
|
|
323
|
+
{
|
|
324
|
+
"schema": "codex-usage-tracker-query-v1",
|
|
325
|
+
"filters": {
|
|
326
|
+
"since": since,
|
|
327
|
+
"until": until,
|
|
328
|
+
"model": model,
|
|
329
|
+
"effort": effort,
|
|
330
|
+
"thread": thread,
|
|
331
|
+
"project": project,
|
|
332
|
+
"pricing_status": pricing_status,
|
|
333
|
+
"credit_confidence": credit_confidence,
|
|
334
|
+
"min_tokens": min_tokens,
|
|
335
|
+
"min_credits": min_credits,
|
|
336
|
+
"limit": normalized_limit,
|
|
337
|
+
"privacy_mode": privacy_mode,
|
|
338
|
+
},
|
|
339
|
+
"row_count": len(limited_rows),
|
|
340
|
+
"total_matched_rows": len(rows),
|
|
341
|
+
"truncated": normalized_limit is not None and len(rows) > normalized_limit,
|
|
342
|
+
"rows": limited_rows,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def build_recommendations_report(
|
|
348
|
+
*,
|
|
349
|
+
db_path: Path,
|
|
350
|
+
pricing_path: Path,
|
|
351
|
+
allowance_path: Path,
|
|
352
|
+
projects_path: Path = DEFAULT_PROJECTS_PATH,
|
|
353
|
+
since: str | None = None,
|
|
354
|
+
until: str | None = None,
|
|
355
|
+
model: str | None = None,
|
|
356
|
+
effort: str | None = None,
|
|
357
|
+
thread: str | None = None,
|
|
358
|
+
project: str | None = None,
|
|
359
|
+
min_score: float | None = None,
|
|
360
|
+
limit: int = 20,
|
|
361
|
+
privacy_mode: str = "normal",
|
|
362
|
+
) -> RecommendationsReport:
|
|
363
|
+
"""Build ranked aggregate recommendations for usage investigations."""
|
|
364
|
+
|
|
365
|
+
privacy_mode = validate_privacy_mode(privacy_mode)
|
|
366
|
+
rows = annotate_thread_attachments(
|
|
367
|
+
query_dashboard_events(
|
|
368
|
+
db_path,
|
|
369
|
+
limit=0,
|
|
370
|
+
since=since,
|
|
371
|
+
until=until,
|
|
372
|
+
model=model,
|
|
373
|
+
effort=effort,
|
|
374
|
+
thread=thread,
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
pricing = load_pricing_config(pricing_path)
|
|
378
|
+
allowance = load_allowance_config(allowance_path)
|
|
379
|
+
rows = annotate_rows_with_allowance(annotate_rows_with_efficiency(rows, pricing), allowance)
|
|
380
|
+
rows = annotate_rows_with_recommendations(rows)
|
|
381
|
+
rows = annotate_rows_with_project_identity(rows, load_project_config(projects_path))
|
|
382
|
+
scored_rows = [
|
|
383
|
+
row
|
|
384
|
+
for row in rows
|
|
385
|
+
if row.get("action_recommendations")
|
|
386
|
+
and (min_score is None or float(row.get("recommendation_score") or 0) >= min_score)
|
|
387
|
+
and _query_row_matches(
|
|
388
|
+
row,
|
|
389
|
+
until=until,
|
|
390
|
+
model=model,
|
|
391
|
+
effort=effort,
|
|
392
|
+
thread=thread,
|
|
393
|
+
project=project,
|
|
394
|
+
pricing_status=None,
|
|
395
|
+
credit_confidence=None,
|
|
396
|
+
min_tokens=None,
|
|
397
|
+
min_credits=None,
|
|
398
|
+
)
|
|
399
|
+
]
|
|
400
|
+
scored_rows.sort(key=_recommendation_sort_key)
|
|
401
|
+
normalized_limit = None if limit <= 0 else limit
|
|
402
|
+
limited_rows = scored_rows if normalized_limit is None else scored_rows[:normalized_limit]
|
|
403
|
+
private_rows = apply_project_privacy_to_rows(limited_rows, privacy_mode=privacy_mode)
|
|
404
|
+
return RecommendationsReport(
|
|
405
|
+
{
|
|
406
|
+
"schema": "codex-usage-tracker-recommendations-v1",
|
|
407
|
+
"filters": {
|
|
408
|
+
"since": since,
|
|
409
|
+
"until": until,
|
|
410
|
+
"model": model,
|
|
411
|
+
"effort": effort,
|
|
412
|
+
"thread": thread,
|
|
413
|
+
"project": project,
|
|
414
|
+
"min_score": min_score,
|
|
415
|
+
"limit": normalized_limit,
|
|
416
|
+
"privacy_mode": privacy_mode,
|
|
417
|
+
},
|
|
418
|
+
"row_count": len(private_rows),
|
|
419
|
+
"total_matched_rows": len(scored_rows),
|
|
420
|
+
"truncated": normalized_limit is not None and len(scored_rows) > normalized_limit,
|
|
421
|
+
"threads": _thread_recommendation_rows(scored_rows, limit=normalized_limit or 20),
|
|
422
|
+
"rows": private_rows,
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _recommendation_sort_key(row: dict[str, Any]) -> tuple[float, int, str, str]:
|
|
428
|
+
return (
|
|
429
|
+
-float(row.get("recommendation_score") or 0),
|
|
430
|
+
-int(row.get("total_tokens") or 0),
|
|
431
|
+
str(row.get("event_timestamp") or ""),
|
|
432
|
+
str(row.get("record_id") or ""),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _thread_recommendation_rows(
|
|
437
|
+
rows: list[dict[str, Any]],
|
|
438
|
+
*,
|
|
439
|
+
limit: int,
|
|
440
|
+
) -> list[dict[str, Any]]:
|
|
441
|
+
buckets: dict[str, dict[str, Any]] = {}
|
|
442
|
+
for row in rows:
|
|
443
|
+
label = str(
|
|
444
|
+
row.get("thread_attachment_label")
|
|
445
|
+
or row.get("thread_name")
|
|
446
|
+
or row.get("resolved_parent_thread_name")
|
|
447
|
+
or row.get("parent_thread_name")
|
|
448
|
+
or row.get("session_id")
|
|
449
|
+
or "Unknown thread"
|
|
450
|
+
)
|
|
451
|
+
bucket = buckets.setdefault(
|
|
452
|
+
label,
|
|
453
|
+
{
|
|
454
|
+
"thread": label,
|
|
455
|
+
"call_count": 0,
|
|
456
|
+
"session_count": set(),
|
|
457
|
+
"total_tokens": 0,
|
|
458
|
+
"estimated_cost_usd": 0.0,
|
|
459
|
+
"usage_credits": 0.0,
|
|
460
|
+
"recommendation_score": 0.0,
|
|
461
|
+
"max_recommendation_score": 0.0,
|
|
462
|
+
"primary_recommendation": None,
|
|
463
|
+
"secondary_signals": set(),
|
|
464
|
+
"latest_event": "",
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
bucket["call_count"] += 1
|
|
468
|
+
bucket["session_count"].add(row.get("session_id"))
|
|
469
|
+
bucket["total_tokens"] += int(row.get("total_tokens") or 0)
|
|
470
|
+
bucket["estimated_cost_usd"] += float(row.get("estimated_cost_usd") or 0)
|
|
471
|
+
bucket["usage_credits"] += float(row.get("usage_credits") or 0)
|
|
472
|
+
score = float(row.get("recommendation_score") or 0)
|
|
473
|
+
bucket["recommendation_score"] += score
|
|
474
|
+
if score > float(bucket["max_recommendation_score"] or 0):
|
|
475
|
+
bucket["max_recommendation_score"] = score
|
|
476
|
+
bucket["primary_recommendation"] = row.get("primary_recommendation")
|
|
477
|
+
if str(row.get("event_timestamp") or "") > str(bucket["latest_event"] or ""):
|
|
478
|
+
bucket["latest_event"] = str(row.get("event_timestamp") or "")
|
|
479
|
+
for signal in row.get("secondary_signals") or []:
|
|
480
|
+
bucket["secondary_signals"].add(signal)
|
|
481
|
+
primary_signal = row.get("primary_signal")
|
|
482
|
+
if primary_signal:
|
|
483
|
+
bucket["secondary_signals"].add(primary_signal)
|
|
484
|
+
summaries: list[dict[str, Any]] = []
|
|
485
|
+
for bucket in buckets.values():
|
|
486
|
+
primary = bucket.get("primary_recommendation")
|
|
487
|
+
primary_key = primary.get("key") if isinstance(primary, dict) else None
|
|
488
|
+
secondary = sorted(str(signal) for signal in bucket["secondary_signals"] if signal)
|
|
489
|
+
if primary_key in secondary:
|
|
490
|
+
secondary.remove(primary_key)
|
|
491
|
+
summaries.append(
|
|
492
|
+
{
|
|
493
|
+
"thread": bucket["thread"],
|
|
494
|
+
"call_count": int(bucket["call_count"]),
|
|
495
|
+
"session_count": len(bucket["session_count"]),
|
|
496
|
+
"total_tokens": int(bucket["total_tokens"]),
|
|
497
|
+
"estimated_cost_usd": round(float(bucket["estimated_cost_usd"]), 6),
|
|
498
|
+
"usage_credits": round(float(bucket["usage_credits"]), 6),
|
|
499
|
+
"recommendation_score": round(float(bucket["recommendation_score"]), 2),
|
|
500
|
+
"max_recommendation_score": round(float(bucket["max_recommendation_score"]), 2),
|
|
501
|
+
"primary_recommendation": primary,
|
|
502
|
+
"secondary_signals": secondary,
|
|
503
|
+
"latest_event": bucket["latest_event"],
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
summaries.sort(
|
|
507
|
+
key=lambda row: (
|
|
508
|
+
-float(row["recommendation_score"]),
|
|
509
|
+
-int(row["total_tokens"]),
|
|
510
|
+
str(row["thread"]),
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
return summaries[:limit]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _query_row_matches(
|
|
517
|
+
row: dict[str, Any],
|
|
518
|
+
*,
|
|
519
|
+
until: str | None,
|
|
520
|
+
model: str | None,
|
|
521
|
+
effort: str | None,
|
|
522
|
+
thread: str | None,
|
|
523
|
+
project: str | None,
|
|
524
|
+
pricing_status: str | None,
|
|
525
|
+
credit_confidence: str | None,
|
|
526
|
+
min_tokens: int | None,
|
|
527
|
+
min_credits: float | None,
|
|
528
|
+
) -> bool:
|
|
529
|
+
if until and str(row.get("event_timestamp") or "") > until:
|
|
530
|
+
return False
|
|
531
|
+
if model and str(row.get("model") or "") != model:
|
|
532
|
+
return False
|
|
533
|
+
if effort and str(row.get("effort") or "") != effort:
|
|
534
|
+
return False
|
|
535
|
+
if thread:
|
|
536
|
+
thread_values = {
|
|
537
|
+
str(row.get("thread_name") or ""),
|
|
538
|
+
str(row.get("parent_thread_name") or ""),
|
|
539
|
+
str(row.get("resolved_parent_thread_name") or ""),
|
|
540
|
+
str(row.get("thread_attachment_label") or ""),
|
|
541
|
+
str(row.get("session_id") or ""),
|
|
542
|
+
}
|
|
543
|
+
if thread not in thread_values:
|
|
544
|
+
return False
|
|
545
|
+
if project:
|
|
546
|
+
project_values = {
|
|
547
|
+
str(row.get("project_name") or ""),
|
|
548
|
+
str(row.get("project_key") or ""),
|
|
549
|
+
str(row.get("project_relative_cwd") or ""),
|
|
550
|
+
}
|
|
551
|
+
if project not in project_values and project not in (row.get("project_tags") or []):
|
|
552
|
+
return False
|
|
553
|
+
if pricing_status == "priced" and not row.get("pricing_model"):
|
|
554
|
+
return False
|
|
555
|
+
if pricing_status == "estimated" and not row.get("pricing_estimated"):
|
|
556
|
+
return False
|
|
557
|
+
if pricing_status == "unpriced" and row.get("pricing_model"):
|
|
558
|
+
return False
|
|
559
|
+
if credit_confidence and row.get("usage_credit_confidence") != credit_confidence:
|
|
560
|
+
return False
|
|
561
|
+
if min_tokens is not None and int(row.get("total_tokens") or 0) < min_tokens:
|
|
562
|
+
return False
|
|
563
|
+
return not (min_credits is not None and float(row.get("usage_credits") or 0) < min_credits)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _project_summary_rows(
|
|
567
|
+
*,
|
|
568
|
+
db_path: Path,
|
|
569
|
+
pricing: PricingConfig,
|
|
570
|
+
group_by: str,
|
|
571
|
+
limit: int,
|
|
572
|
+
since: str | None,
|
|
573
|
+
projects_path: Path = DEFAULT_PROJECTS_PATH,
|
|
574
|
+
privacy_mode: str = "normal",
|
|
575
|
+
) -> list[dict[str, Any]]:
|
|
576
|
+
rows = annotate_rows_with_project_identity(
|
|
577
|
+
annotate_rows_with_efficiency(query_dashboard_events(db_path, limit=0, since=since), pricing),
|
|
578
|
+
load_project_config(projects_path),
|
|
579
|
+
)
|
|
580
|
+
rows = apply_project_privacy_to_rows(rows, privacy_mode=privacy_mode)
|
|
581
|
+
buckets: dict[str, dict[str, Any]] = {}
|
|
582
|
+
for row in rows:
|
|
583
|
+
if group_by == "project_tag":
|
|
584
|
+
keys = row.get("project_tags") or ["untagged"]
|
|
585
|
+
else:
|
|
586
|
+
keys = [row.get("project_name") or "Unknown project"]
|
|
587
|
+
for key in keys:
|
|
588
|
+
bucket = buckets.setdefault(
|
|
589
|
+
str(key),
|
|
590
|
+
{
|
|
591
|
+
"group_key": str(key),
|
|
592
|
+
"model_calls": 0,
|
|
593
|
+
"sessions": set(),
|
|
594
|
+
"turns": set(),
|
|
595
|
+
"input_tokens": 0,
|
|
596
|
+
"cached_input_tokens": 0,
|
|
597
|
+
"uncached_input_tokens": 0,
|
|
598
|
+
"output_tokens": 0,
|
|
599
|
+
"reasoning_output_tokens": 0,
|
|
600
|
+
"total_tokens": 0,
|
|
601
|
+
"estimated_cost_usd": 0.0,
|
|
602
|
+
"_cache_ratio_sum": 0.0,
|
|
603
|
+
"_reasoning_ratio_sum": 0.0,
|
|
604
|
+
"_context_sum": 0.0,
|
|
605
|
+
"latest_event": "",
|
|
606
|
+
},
|
|
607
|
+
)
|
|
608
|
+
bucket["model_calls"] += 1
|
|
609
|
+
bucket["sessions"].add(row.get("session_id"))
|
|
610
|
+
if row.get("turn_id"):
|
|
611
|
+
bucket["turns"].add(row.get("turn_id"))
|
|
612
|
+
for token_key in (
|
|
613
|
+
"input_tokens",
|
|
614
|
+
"cached_input_tokens",
|
|
615
|
+
"uncached_input_tokens",
|
|
616
|
+
"output_tokens",
|
|
617
|
+
"reasoning_output_tokens",
|
|
618
|
+
"total_tokens",
|
|
619
|
+
):
|
|
620
|
+
bucket[token_key] += int(row.get(token_key) or 0)
|
|
621
|
+
bucket["estimated_cost_usd"] += float(row.get("estimated_cost_usd") or 0)
|
|
622
|
+
bucket["_cache_ratio_sum"] += float(row.get("cache_ratio") or 0)
|
|
623
|
+
bucket["_reasoning_ratio_sum"] += float(row.get("reasoning_output_ratio") or 0)
|
|
624
|
+
bucket["_context_sum"] += float(row.get("context_window_percent") or 0)
|
|
625
|
+
if str(row.get("event_timestamp") or "") > bucket["latest_event"]:
|
|
626
|
+
bucket["latest_event"] = str(row.get("event_timestamp") or "")
|
|
627
|
+
summaries: list[dict[str, Any]] = []
|
|
628
|
+
for bucket in buckets.values():
|
|
629
|
+
calls = max(int(bucket["model_calls"]), 1)
|
|
630
|
+
bucket["sessions"] = len(bucket["sessions"])
|
|
631
|
+
bucket["turns"] = len(bucket["turns"])
|
|
632
|
+
bucket["avg_cache_ratio"] = bucket.pop("_cache_ratio_sum") / calls
|
|
633
|
+
bucket["avg_reasoning_output_ratio"] = bucket.pop("_reasoning_ratio_sum") / calls
|
|
634
|
+
bucket["avg_context_window_percent"] = bucket.pop("_context_sum") / calls
|
|
635
|
+
summaries.append(bucket)
|
|
636
|
+
summaries.sort(key=lambda row: (-int(row["total_tokens"]), str(row["group_key"])))
|
|
637
|
+
return summaries[:limit]
|