cctally 1.73.0 → 1.75.0
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.
- package/CHANGELOG.md +24 -0
- package/bin/_cctally_cache.py +35 -5
- package/bin/_cctally_config.py +3 -3
- package/bin/_cctally_core.py +12 -3
- package/bin/_cctally_dashboard.py +57 -5
- package/bin/_cctally_dashboard_conversation.py +2 -2
- package/bin/_cctally_dashboard_share.py +38 -3
- package/bin/_cctally_dashboard_sources.py +1199 -78
- package/bin/_cctally_db.py +798 -2
- package/bin/_cctally_doctor.py +27 -4
- package/bin/_cctally_parser.py +100 -6
- package/bin/_cctally_percent_breakdown.py +59 -34
- package/bin/_cctally_quota.py +311 -45
- package/bin/_cctally_record.py +52 -1
- package/bin/_cctally_refresh.py +26 -0
- package/bin/_cctally_source_analytics.py +288 -3
- package/bin/_cctally_statusline.py +3 -3
- package/bin/_cctally_tui.py +20 -14
- package/bin/_lib_aggregators.py +55 -34
- package/bin/_lib_cache_report.py +25 -9
- package/bin/_lib_conversation_retention.py +22 -2
- package/bin/_lib_dashboard_sources.py +20 -7
- package/bin/_lib_doctor.py +48 -4
- package/bin/_lib_jsonl.py +11 -0
- package/bin/_lib_pricing.py +7 -4
- package/bin/_lib_pricing_check.py +7 -1
- package/bin/_lib_quota.py +12 -11
- package/bin/_lib_view_models.py +50 -14
- package/bin/cctally +27 -1
- package/dashboard/static/assets/index-fZRxsSf5.js +80 -0
- package/dashboard/static/assets/{index-D2nwo_ln.css → index-yftBNnLR.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-BWadAHxC.js +0 -80
|
@@ -4,25 +4,36 @@ from __future__ import annotations
|
|
|
4
4
|
import datetime as dt
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import pathlib
|
|
7
8
|
import sqlite3
|
|
8
9
|
import sys
|
|
9
10
|
from collections.abc import Mapping
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from types import MappingProxyType
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
|
+
from types import MappingProxyType, SimpleNamespace
|
|
12
13
|
from typing import Any, Iterable
|
|
13
14
|
from zoneinfo import ZoneInfo
|
|
14
15
|
|
|
15
16
|
from _cctally_core import get_week_start_name
|
|
16
17
|
from _cctally_quota import (
|
|
18
|
+
codex_five_hour_percent_at_crossing,
|
|
19
|
+
codex_quota_breakdown,
|
|
17
20
|
codex_physical_mutation_seq,
|
|
18
21
|
load_codex_quota_observations,
|
|
19
22
|
load_codex_quota_projection_certificate,
|
|
20
23
|
)
|
|
21
|
-
from _cctally_source_analytics import
|
|
24
|
+
from _cctally_source_analytics import (
|
|
25
|
+
QualifiedMetadataUnavailable,
|
|
26
|
+
has_cached_codex_accounting_entries,
|
|
27
|
+
load_cached_rooted_codex_accounting_entries,
|
|
28
|
+
load_codex_project_metadata_health,
|
|
29
|
+
load_qualified_codex_entries,
|
|
30
|
+
)
|
|
31
|
+
import _lib_log
|
|
22
32
|
from _lib_dashboard_sources import (
|
|
23
33
|
CapabilityRecord,
|
|
24
34
|
ProjectionCoherence,
|
|
25
35
|
SourceDashboardState,
|
|
36
|
+
SourceDashboardWarning,
|
|
26
37
|
assess_codex_projection_coherence,
|
|
27
38
|
dashboard_resource_key,
|
|
28
39
|
)
|
|
@@ -35,12 +46,16 @@ from _lib_quota import (
|
|
|
35
46
|
select_baseline,
|
|
36
47
|
)
|
|
37
48
|
from _lib_jsonl import CodexEntry
|
|
49
|
+
from _lib_fmt import stable_sum
|
|
50
|
+
from _lib_aggregators import _aggregate_codex_buckets
|
|
51
|
+
from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
38
52
|
from _lib_source_analytics import build_codex_project_result
|
|
39
53
|
from _lib_view_models import (
|
|
54
|
+
CodexWeeklyView,
|
|
40
55
|
build_codex_daily_view,
|
|
41
56
|
build_codex_monthly_view,
|
|
57
|
+
build_rooted_codex_session_view,
|
|
42
58
|
build_codex_session_view,
|
|
43
|
-
build_codex_weekly_view,
|
|
44
59
|
)
|
|
45
60
|
|
|
46
61
|
|
|
@@ -50,12 +65,12 @@ DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
|
|
|
50
65
|
DASHBOARD_QUOTA_RECENT_DAYS = 35
|
|
51
66
|
|
|
52
67
|
|
|
53
|
-
class
|
|
54
|
-
"""
|
|
68
|
+
class CodexCycleUnavailable(RuntimeError):
|
|
69
|
+
"""No single active native seven-day boundary can bound hero accounting."""
|
|
55
70
|
|
|
56
|
-
def __init__(self, reason: str
|
|
71
|
+
def __init__(self, reason: str) -> None:
|
|
57
72
|
self.reason = reason
|
|
58
|
-
super().__init__(
|
|
73
|
+
super().__init__(reason)
|
|
59
74
|
|
|
60
75
|
|
|
61
76
|
class SourceCapabilityUnavailable(ValueError):
|
|
@@ -66,6 +81,173 @@ class SourceResourceNotFound(LookupError):
|
|
|
66
81
|
"""A valid opaque resource key has no row in its provider state."""
|
|
67
82
|
|
|
68
83
|
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class CodexCycleBoundary:
|
|
86
|
+
"""The one active native subscription cycle usable for hero accounting."""
|
|
87
|
+
|
|
88
|
+
window_minutes: int
|
|
89
|
+
start_at: dt.datetime
|
|
90
|
+
resets_at: dt.datetime
|
|
91
|
+
# Root provenance is server-only accounting input, never public wire data.
|
|
92
|
+
source_root_keys: tuple[str, ...]
|
|
93
|
+
used_percent: float | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class CodexWeeklyPeriod:
|
|
98
|
+
"""One non-overlapping observed native seven-day quota cycle segment."""
|
|
99
|
+
|
|
100
|
+
start_at: dt.datetime
|
|
101
|
+
end_at: dt.datetime
|
|
102
|
+
source_root_keys: tuple[str, ...]
|
|
103
|
+
used_percent: float | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_codex_weekly_cycle(
|
|
107
|
+
observations: Iterable[object],
|
|
108
|
+
now_utc: dt.datetime,
|
|
109
|
+
) -> CodexCycleBoundary:
|
|
110
|
+
"""Select exactly one active account-level 10,080-minute native cycle."""
|
|
111
|
+
boundaries: dict[tuple[int, dt.datetime], dict[str, object]] = {}
|
|
112
|
+
stale_weekly_evidence = False
|
|
113
|
+
for history in build_history(tuple(observations)):
|
|
114
|
+
if history.identity.window_minutes != 10_080:
|
|
115
|
+
continue
|
|
116
|
+
baseline = select_baseline(history.observations, now_utc)
|
|
117
|
+
if baseline is None or baseline.resets_at <= now_utc:
|
|
118
|
+
continue
|
|
119
|
+
if quota_freshness(history.physical_observations, now_utc).state != "fresh":
|
|
120
|
+
stale_weekly_evidence = True
|
|
121
|
+
continue
|
|
122
|
+
boundary = (history.identity.window_minutes, baseline.resets_at)
|
|
123
|
+
item = boundaries.setdefault(boundary, {"roots": set(), "used": []})
|
|
124
|
+
item["roots"].add(history.identity.source_root_key)
|
|
125
|
+
if baseline.used_percent is not None:
|
|
126
|
+
item["used"].append(float(baseline.used_percent))
|
|
127
|
+
if len(boundaries) != 1:
|
|
128
|
+
if not boundaries:
|
|
129
|
+
raise CodexCycleUnavailable("stale" if stale_weekly_evidence else "missing")
|
|
130
|
+
raise CodexCycleUnavailable("conflicting")
|
|
131
|
+
(window_minutes, resets_at), boundary_data = next(iter(boundaries.items()))
|
|
132
|
+
root_keys = boundary_data["roots"]
|
|
133
|
+
used_values = boundary_data["used"]
|
|
134
|
+
return CodexCycleBoundary(
|
|
135
|
+
window_minutes=window_minutes,
|
|
136
|
+
start_at=resets_at - dt.timedelta(minutes=window_minutes),
|
|
137
|
+
resets_at=resets_at,
|
|
138
|
+
source_root_keys=tuple(sorted(root_keys)),
|
|
139
|
+
used_percent=max(used_values) if used_values else None,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _codex_weekly_periods(
|
|
144
|
+
stats_conn: sqlite3.Connection,
|
|
145
|
+
*,
|
|
146
|
+
source_root_keys: Iterable[str],
|
|
147
|
+
active_cycle: CodexCycleBoundary | None,
|
|
148
|
+
) -> tuple[CodexWeeklyPeriod, ...]:
|
|
149
|
+
"""Read durable 10,080-minute boundaries and clip early re-anchors.
|
|
150
|
+
|
|
151
|
+
A provider-granted reset changes the native window's nominal start before
|
|
152
|
+
the prior seven-day deadline. Sorting those nominal starts and ending the
|
|
153
|
+
prior segment at the next start preserves the actual quota-cycle boundary
|
|
154
|
+
without double-counting the overlapping nominal windows.
|
|
155
|
+
"""
|
|
156
|
+
roots = tuple(sorted({
|
|
157
|
+
root for root in source_root_keys if isinstance(root, str) and root
|
|
158
|
+
}))
|
|
159
|
+
if not roots:
|
|
160
|
+
return ()
|
|
161
|
+
placeholders = ",".join("?" for _ in roots)
|
|
162
|
+
try:
|
|
163
|
+
rows = stats_conn.execute(
|
|
164
|
+
"SELECT source_root_key, resets_at_utc, nominal_start_at_utc, current_percent "
|
|
165
|
+
"FROM quota_window_blocks "
|
|
166
|
+
"WHERE source='codex' AND window_minutes=10080 "
|
|
167
|
+
f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
|
|
168
|
+
"ORDER BY nominal_start_at_utc DESC, resets_at_utc DESC, source_root_key "
|
|
169
|
+
"LIMIT ?",
|
|
170
|
+
(*roots, SOURCE_HISTORY_LIMIT),
|
|
171
|
+
).fetchall()
|
|
172
|
+
except sqlite3.Error:
|
|
173
|
+
rows = ()
|
|
174
|
+
|
|
175
|
+
raw_boundaries: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
|
|
176
|
+
|
|
177
|
+
for root_key, resets_at_raw, start_at_raw, current_percent in rows:
|
|
178
|
+
try:
|
|
179
|
+
start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
|
|
180
|
+
resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
|
|
181
|
+
except (TypeError, ValueError):
|
|
182
|
+
continue
|
|
183
|
+
if start_at.tzinfo is None or resets_at.tzinfo is None:
|
|
184
|
+
continue
|
|
185
|
+
start_at = start_at.astimezone(UTC)
|
|
186
|
+
resets_at = resets_at.astimezone(UTC)
|
|
187
|
+
if resets_at <= start_at:
|
|
188
|
+
continue
|
|
189
|
+
used_values = []
|
|
190
|
+
if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
|
|
191
|
+
used_values.append(float(current_percent))
|
|
192
|
+
raw_boundaries.append((start_at, resets_at, {str(root_key)}, used_values))
|
|
193
|
+
|
|
194
|
+
if active_cycle is not None:
|
|
195
|
+
raw_boundaries.append((
|
|
196
|
+
active_cycle.start_at.astimezone(UTC),
|
|
197
|
+
active_cycle.resets_at.astimezone(UTC),
|
|
198
|
+
set(active_cycle.source_root_keys),
|
|
199
|
+
[active_cycle.used_percent] if active_cycle.used_percent is not None else [],
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
ordered: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
|
|
203
|
+
for start_at, resets_at, period_roots, used_values in sorted(
|
|
204
|
+
raw_boundaries, key=lambda item: (item[0], item[1]),
|
|
205
|
+
):
|
|
206
|
+
if (
|
|
207
|
+
ordered
|
|
208
|
+
and (start_at - ordered[-1][0]).total_seconds()
|
|
209
|
+
< _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
210
|
+
):
|
|
211
|
+
first_start, latest_reset, existing_roots, existing_used = ordered[-1]
|
|
212
|
+
existing_roots.update(period_roots)
|
|
213
|
+
existing_used.extend(used_values)
|
|
214
|
+
ordered[-1] = (
|
|
215
|
+
first_start, max(latest_reset, resets_at), existing_roots, existing_used,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
ordered.append((start_at, resets_at, set(period_roots), list(used_values)))
|
|
219
|
+
periods: list[CodexWeeklyPeriod] = []
|
|
220
|
+
for index, (start_at, resets_at, period_roots, used_values) in enumerate(ordered):
|
|
221
|
+
next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
|
|
222
|
+
end_at = min(resets_at, next_start) if next_start is not None else resets_at
|
|
223
|
+
if end_at <= start_at:
|
|
224
|
+
continue
|
|
225
|
+
periods.append(CodexWeeklyPeriod(
|
|
226
|
+
start_at=start_at,
|
|
227
|
+
end_at=end_at,
|
|
228
|
+
source_root_keys=tuple(sorted(period_roots)),
|
|
229
|
+
used_percent=max(used_values) if used_values else None,
|
|
230
|
+
))
|
|
231
|
+
return tuple(periods)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _native_limit_label(limit_name: object, window_minutes: object) -> str:
|
|
235
|
+
"""Prefer provider label text, deriving duration copy only when absent."""
|
|
236
|
+
if isinstance(limit_name, str) and limit_name.strip():
|
|
237
|
+
return limit_name.strip()
|
|
238
|
+
if window_minutes == 300:
|
|
239
|
+
return "5-hour limit"
|
|
240
|
+
if window_minutes == 10_080:
|
|
241
|
+
return "7-day limit"
|
|
242
|
+
if not isinstance(window_minutes, int) or isinstance(window_minutes, bool) or window_minutes <= 0:
|
|
243
|
+
return "Codex quota"
|
|
244
|
+
if window_minutes % 1_440 == 0:
|
|
245
|
+
return f"{window_minutes // 1_440}-day limit"
|
|
246
|
+
if window_minutes % 60 == 0:
|
|
247
|
+
return f"{window_minutes // 60}-hour limit"
|
|
248
|
+
return f"{window_minutes}-minute limit"
|
|
249
|
+
|
|
250
|
+
|
|
69
251
|
@dataclass(frozen=True)
|
|
70
252
|
class DashboardSourceSemantics:
|
|
71
253
|
"""One canonical CLI configuration resolution for a dashboard read.
|
|
@@ -81,6 +263,9 @@ class DashboardSourceSemantics:
|
|
|
81
263
|
week_start_idx: int
|
|
82
264
|
speed: str
|
|
83
265
|
codex_budget: Mapping[str, object] | None
|
|
266
|
+
codex_quota_actual_thresholds: tuple[int, ...]
|
|
267
|
+
codex_quota_projected_thresholds: tuple[int, ...]
|
|
268
|
+
cache_report_anomaly_threshold_pp: int
|
|
84
269
|
claude_identity: str
|
|
85
270
|
codex_identity: str
|
|
86
271
|
|
|
@@ -103,6 +288,17 @@ def resolve_dashboard_source_semantics(
|
|
|
103
288
|
week_start_idx = c.WEEKDAY_MAP[week_start_name]
|
|
104
289
|
speed = c._resolve_codex_speed("auto")
|
|
105
290
|
budget_config = c._get_budget_config(raw_config)
|
|
291
|
+
quota_alerts = c._get_quota_alerts_config(raw_config)
|
|
292
|
+
raw_cache_report = raw_config.get("cache_report")
|
|
293
|
+
raw_cache_threshold = (
|
|
294
|
+
raw_cache_report.get("anomaly_threshold_pp", 15)
|
|
295
|
+
if isinstance(raw_cache_report, Mapping) else 15
|
|
296
|
+
)
|
|
297
|
+
cache_threshold = (
|
|
298
|
+
int(raw_cache_threshold)
|
|
299
|
+
if isinstance(raw_cache_threshold, int) and not isinstance(raw_cache_threshold, bool)
|
|
300
|
+
and 1 <= raw_cache_threshold <= 100 else 15
|
|
301
|
+
)
|
|
106
302
|
raw_codex_budget = budget_config.get("codex")
|
|
107
303
|
codex_budget = (
|
|
108
304
|
MappingProxyType(dict(raw_codex_budget))
|
|
@@ -126,6 +322,8 @@ def resolve_dashboard_source_semantics(
|
|
|
126
322
|
}
|
|
127
323
|
codex_identity_payload = {
|
|
128
324
|
"codex_budget": dict(codex_budget) if codex_budget is not None else None,
|
|
325
|
+
"codex_quota_alerts": quota_alerts,
|
|
326
|
+
"cache_report_anomaly_threshold_pp": cache_threshold,
|
|
129
327
|
"display_tz_name": display_tz_name,
|
|
130
328
|
"speed": speed,
|
|
131
329
|
"week_start_name": week_start_name,
|
|
@@ -142,6 +340,9 @@ def resolve_dashboard_source_semantics(
|
|
|
142
340
|
week_start_idx=week_start_idx,
|
|
143
341
|
speed=speed,
|
|
144
342
|
codex_budget=codex_budget,
|
|
343
|
+
codex_quota_actual_thresholds=tuple(quota_alerts["actual_thresholds"]),
|
|
344
|
+
codex_quota_projected_thresholds=tuple(quota_alerts["projected_thresholds"]),
|
|
345
|
+
cache_report_anomaly_threshold_pp=cache_threshold,
|
|
145
346
|
claude_identity=claude_identity,
|
|
146
347
|
codex_identity=codex_identity,
|
|
147
348
|
)
|
|
@@ -160,6 +361,9 @@ class DashboardReadContext:
|
|
|
160
361
|
week_start_name: str = "monday"
|
|
161
362
|
speed: str = "standard"
|
|
162
363
|
codex_budget: Mapping[str, object] | None = None
|
|
364
|
+
codex_quota_actual_thresholds: tuple[int, ...] = ()
|
|
365
|
+
codex_quota_projected_thresholds: tuple[int, ...] = ()
|
|
366
|
+
cache_report_anomaly_threshold_pp: int = 15
|
|
163
367
|
|
|
164
368
|
def __post_init__(self) -> None:
|
|
165
369
|
for name in ("range_start", "now_utc"):
|
|
@@ -289,7 +493,7 @@ def _codex_budget_cost_events(
|
|
|
289
493
|
|
|
290
494
|
|
|
291
495
|
def _bucket_wire(bucket: Any) -> dict[str, object]:
|
|
292
|
-
|
|
496
|
+
result = {
|
|
293
497
|
"label": bucket.bucket,
|
|
294
498
|
"cost_usd": bucket.cost_usd,
|
|
295
499
|
"input_tokens": bucket.input_tokens,
|
|
@@ -298,7 +502,17 @@ def _bucket_wire(bucket: Any) -> dict[str, object]:
|
|
|
298
502
|
"reasoning_output_tokens": bucket.reasoning_output_tokens,
|
|
299
503
|
"total_tokens": bucket.total_tokens,
|
|
300
504
|
"models": tuple(bucket.models),
|
|
505
|
+
"model_breakdowns": tuple(dict(row) for row in bucket.model_breakdowns),
|
|
301
506
|
}
|
|
507
|
+
for name in ("period_start_at", "period_end_at"):
|
|
508
|
+
value = getattr(bucket, name, None)
|
|
509
|
+
if isinstance(value, dt.datetime):
|
|
510
|
+
result[name.replace("period_", "")] = value.astimezone(UTC).isoformat()
|
|
511
|
+
for name in ("used_pct", "dollar_per_pct"):
|
|
512
|
+
value = getattr(bucket, name, None)
|
|
513
|
+
if value is not None:
|
|
514
|
+
result[name] = value
|
|
515
|
+
return result
|
|
302
516
|
|
|
303
517
|
|
|
304
518
|
def _period_wire(view: Any) -> dict[str, object]:
|
|
@@ -310,20 +524,361 @@ def _period_wire(view: Any) -> dict[str, object]:
|
|
|
310
524
|
}
|
|
311
525
|
|
|
312
526
|
|
|
313
|
-
def
|
|
527
|
+
def _codex_cache_report_wire(
|
|
528
|
+
entries: Iterable[object],
|
|
529
|
+
*,
|
|
530
|
+
metadata: Mapping[tuple[str, str], Mapping[str, object]],
|
|
531
|
+
now_utc: dt.datetime,
|
|
532
|
+
display_tz_name: str | None,
|
|
533
|
+
speed: str,
|
|
534
|
+
anomaly_threshold_pp: int = 15,
|
|
535
|
+
window_days: int = 14,
|
|
536
|
+
) -> dict[str, object]:
|
|
537
|
+
"""Compute the canonical cache report from Codex's inclusive counters.
|
|
538
|
+
|
|
539
|
+
Codex input is cache-inclusive, so the shared cache-report kernel receives
|
|
540
|
+
uncached input plus cached input as two disjoint counters. OpenAI does not
|
|
541
|
+
charge a cache-write premium; the counterfactual is therefore the exact
|
|
542
|
+
uncached-vs-cached input price difference for each token-count event.
|
|
543
|
+
"""
|
|
544
|
+
c = sys.modules["cctally"]
|
|
545
|
+
crk = c._load_sibling("_lib_cache_report")
|
|
546
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
547
|
+
cutoff = now_utc - dt.timedelta(days=window_days)
|
|
548
|
+
|
|
549
|
+
def _tiered_cost(tokens: int, pricing: Mapping[str, object], base: str, above: str) -> float:
|
|
550
|
+
if tokens <= 0:
|
|
551
|
+
return 0.0
|
|
552
|
+
base_rate = float(pricing.get(base, 0.0) or 0.0)
|
|
553
|
+
above_rate = pricing.get(above)
|
|
554
|
+
threshold = int(c.CODEX_TIERED_THRESHOLD)
|
|
555
|
+
if tokens > threshold and above_rate is not None:
|
|
556
|
+
return threshold * base_rate + (tokens - threshold) * float(above_rate)
|
|
557
|
+
return tokens * base_rate
|
|
558
|
+
|
|
559
|
+
wrapped = []
|
|
560
|
+
for entry in entries:
|
|
561
|
+
timestamp = getattr(entry, "timestamp", None)
|
|
562
|
+
if not isinstance(timestamp, dt.datetime) or timestamp < cutoff:
|
|
563
|
+
continue
|
|
564
|
+
model = str(getattr(entry, "model", "") or "unknown")
|
|
565
|
+
input_tokens = int(getattr(entry, "input_tokens", 0))
|
|
566
|
+
cached_tokens = min(input_tokens, int(getattr(entry, "cached_input_tokens", 0)))
|
|
567
|
+
uncached_tokens = max(0, input_tokens - cached_tokens)
|
|
568
|
+
pricing, _is_fallback = c._resolve_codex_pricing(model)
|
|
569
|
+
pricing = pricing or {}
|
|
570
|
+
uncached_counterfactual = _tiered_cost(
|
|
571
|
+
cached_tokens, pricing,
|
|
572
|
+
"input_cost_per_token", "input_cost_per_token_above_272k_tokens",
|
|
573
|
+
)
|
|
574
|
+
cached_actual = _tiered_cost(
|
|
575
|
+
cached_tokens, pricing,
|
|
576
|
+
"cache_read_input_token_cost", "cache_read_input_token_cost_above_272k_tokens",
|
|
577
|
+
)
|
|
578
|
+
multiplier = c._codex_fast_multiplier(model) if speed == "fast" else 1.0
|
|
579
|
+
saved = max(0.0, uncached_counterfactual - cached_actual) * multiplier
|
|
580
|
+
identity = (
|
|
581
|
+
str(getattr(entry, "source_root_key", "") or ""),
|
|
582
|
+
str(getattr(entry, "source_path", "") or ""),
|
|
583
|
+
)
|
|
584
|
+
item_metadata = metadata.get(identity) or {}
|
|
585
|
+
project = (
|
|
586
|
+
str(getattr(entry, "project_label", "") or "").strip()
|
|
587
|
+
or str(item_metadata.get("project_label") or "").strip()
|
|
588
|
+
or "(unknown)"
|
|
589
|
+
)
|
|
590
|
+
wrapped.append(SimpleNamespace(
|
|
591
|
+
timestamp=timestamp,
|
|
592
|
+
model=model,
|
|
593
|
+
cost_usd=float(getattr(entry, "cost_usd", 0.0)),
|
|
594
|
+
project_path=project,
|
|
595
|
+
input_tokens=uncached_tokens,
|
|
596
|
+
output_tokens=int(getattr(entry, "output_tokens", 0)),
|
|
597
|
+
cache_creation_tokens=0,
|
|
598
|
+
cache_read_tokens=cached_tokens,
|
|
599
|
+
cache_saved_usd=saved,
|
|
600
|
+
cache_wasted_usd=0.0,
|
|
601
|
+
cache_net_usd=saved,
|
|
602
|
+
usage={
|
|
603
|
+
"input_tokens": uncached_tokens,
|
|
604
|
+
"output_tokens": int(getattr(entry, "output_tokens", 0)),
|
|
605
|
+
"cache_creation_input_tokens": 0,
|
|
606
|
+
"cache_read_input_tokens": cached_tokens,
|
|
607
|
+
},
|
|
608
|
+
))
|
|
609
|
+
|
|
610
|
+
today_iso = now_utc.astimezone(display_tz or UTC).strftime("%Y-%m-%d")
|
|
611
|
+
if not wrapped:
|
|
612
|
+
return {
|
|
613
|
+
"window_days": window_days,
|
|
614
|
+
"anomaly_threshold_pp": anomaly_threshold_pp,
|
|
615
|
+
"anomaly_window_days": window_days,
|
|
616
|
+
"today": {
|
|
617
|
+
"date": today_iso, "cache_hit_percent": 0.0,
|
|
618
|
+
"baseline_median_percent": None, "delta_pp": None,
|
|
619
|
+
"net_usd": 0.0, "saved_usd": 0.0, "wasted_usd": 0.0,
|
|
620
|
+
"anomaly_triggered": False, "anomaly_reasons": (),
|
|
621
|
+
"baseline_daily_row_count": 0,
|
|
622
|
+
},
|
|
623
|
+
"days": (), "by_project": (), "by_model": (),
|
|
624
|
+
"seven_day_net_usd": 0.0, "seven_day_anomaly_count": 0,
|
|
625
|
+
"fourteen_day_counterfactual_usd": 0.0,
|
|
626
|
+
"fourteen_day_efficiency_ratio": 0.0, "is_empty": True,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
result = crk._build_cache_report(
|
|
630
|
+
wrapped,
|
|
631
|
+
now_utc=now_utc,
|
|
632
|
+
window_days=window_days,
|
|
633
|
+
anomaly_threshold_pp=anomaly_threshold_pp,
|
|
634
|
+
anomaly_window_days=window_days,
|
|
635
|
+
display_tz=display_tz,
|
|
636
|
+
pricing=c.CODEX_MODEL_PRICING,
|
|
637
|
+
cost_calculator=lambda _model, _usage, _mode, cost: float(cost or 0.0),
|
|
638
|
+
)
|
|
639
|
+
raw_rows = sorted(result.rows, key=lambda row: row.date or "", reverse=True)
|
|
640
|
+
days = tuple({
|
|
641
|
+
"date": row.date or "",
|
|
642
|
+
"cache_hit_percent": row.cache_hit_percent,
|
|
643
|
+
"input_tokens": row.input_tokens,
|
|
644
|
+
"output_tokens": row.output_tokens,
|
|
645
|
+
"cache_creation_tokens": row.cache_creation_tokens,
|
|
646
|
+
"cache_read_tokens": row.cache_read_tokens,
|
|
647
|
+
"saved_usd": row.saved_usd,
|
|
648
|
+
"wasted_usd": row.wasted_usd,
|
|
649
|
+
"net_usd": row.net_usd,
|
|
650
|
+
"anomaly_triggered": row.anomaly_triggered,
|
|
651
|
+
"anomaly_reasons": tuple(row.anomaly_reasons),
|
|
652
|
+
} for row in raw_rows[:window_days])
|
|
653
|
+
today_row = next((row for row in raw_rows if row.date == today_iso), None)
|
|
654
|
+
baseline_count = sum(1 for row in raw_rows if row.date != today_iso)
|
|
655
|
+
baseline = result.today_baseline_median
|
|
656
|
+
today_hit = today_row.cache_hit_percent if today_row else 0.0
|
|
657
|
+
kept_dates = {row["date"] for row in days}
|
|
658
|
+
kept_entries = [
|
|
659
|
+
entry for entry in wrapped
|
|
660
|
+
if entry.timestamp.astimezone(display_tz or UTC).strftime("%Y-%m-%d") in kept_dates
|
|
661
|
+
]
|
|
662
|
+
by_project = crk._aggregate_cache_breakdown(
|
|
663
|
+
kept_entries, key_fn=lambda entry: entry.project_path,
|
|
664
|
+
pricing=c.CODEX_MODEL_PRICING,
|
|
665
|
+
)
|
|
666
|
+
by_model = crk._aggregate_cache_breakdown(
|
|
667
|
+
kept_entries, key_fn=lambda entry: entry.model,
|
|
668
|
+
pricing=c.CODEX_MODEL_PRICING,
|
|
669
|
+
)
|
|
670
|
+
seven = days[:7]
|
|
671
|
+
saved_total = stable_sum(float(row["saved_usd"]) for row in days)
|
|
672
|
+
wasted_total = stable_sum(float(row["wasted_usd"]) for row in days)
|
|
673
|
+
efficiency_denom = saved_total + abs(wasted_total)
|
|
674
|
+
return {
|
|
675
|
+
"window_days": window_days,
|
|
676
|
+
"anomaly_threshold_pp": anomaly_threshold_pp,
|
|
677
|
+
"anomaly_window_days": window_days,
|
|
678
|
+
"today": {
|
|
679
|
+
"date": today_iso,
|
|
680
|
+
"cache_hit_percent": today_hit,
|
|
681
|
+
"baseline_median_percent": baseline,
|
|
682
|
+
"delta_pp": today_hit - baseline if baseline is not None else None,
|
|
683
|
+
"net_usd": today_row.net_usd if today_row else 0.0,
|
|
684
|
+
"saved_usd": today_row.saved_usd if today_row else 0.0,
|
|
685
|
+
"wasted_usd": today_row.wasted_usd if today_row else 0.0,
|
|
686
|
+
"anomaly_triggered": today_row.anomaly_triggered if today_row else False,
|
|
687
|
+
"anomaly_reasons": tuple(today_row.anomaly_reasons) if today_row else (),
|
|
688
|
+
"baseline_daily_row_count": baseline_count,
|
|
689
|
+
},
|
|
690
|
+
"days": days,
|
|
691
|
+
"by_project": tuple({
|
|
692
|
+
"key": row.key, "cache_hit_percent": row.cache_hit_percent,
|
|
693
|
+
"net_usd": row.net_usd,
|
|
694
|
+
} for row in by_project),
|
|
695
|
+
"by_model": tuple({
|
|
696
|
+
"key": row.key, "cache_hit_percent": row.cache_hit_percent,
|
|
697
|
+
"net_usd": row.net_usd,
|
|
698
|
+
} for row in by_model),
|
|
699
|
+
"seven_day_net_usd": stable_sum(float(row["net_usd"]) for row in seven),
|
|
700
|
+
"seven_day_anomaly_count": sum(bool(row["anomaly_triggered"]) for row in seven),
|
|
701
|
+
"fourteen_day_counterfactual_usd": saved_total,
|
|
702
|
+
"fourteen_day_efficiency_ratio": (
|
|
703
|
+
saved_total / efficiency_denom if efficiency_denom > 1e-9 else 0.0
|
|
704
|
+
),
|
|
705
|
+
"is_empty": False,
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _codex_conversation_metadata(
|
|
710
|
+
cache_conn: sqlite3.Connection,
|
|
711
|
+
) -> dict[tuple[str, str], dict[str, object]]:
|
|
712
|
+
"""Read task short names and cached project metadata by rooted rollout.
|
|
713
|
+
|
|
714
|
+
``state_5.sqlite.threads.title`` is Codex's persisted user-facing task name.
|
|
715
|
+
Conversation rollup titles are derived from prompt text and therefore must
|
|
716
|
+
never be substituted for that name on the dashboard. Accounting remains
|
|
717
|
+
authoritative for totals; conversation rollups only decorate projects.
|
|
718
|
+
"""
|
|
719
|
+
metadata: dict[tuple[str, str], dict[str, object]] = {}
|
|
720
|
+
try:
|
|
721
|
+
rows = tuple(cache_conn.execute(
|
|
722
|
+
"SELECT t.source_root_key, t.source_path, t.native_thread_id, "
|
|
723
|
+
"(SELECT e.session_id FROM codex_session_entries AS e "
|
|
724
|
+
" WHERE e.source_root_key=t.source_root_key AND e.source_path=t.source_path "
|
|
725
|
+
" ORDER BY e.id LIMIT 1) AS accounting_session_id, "
|
|
726
|
+
"r.project_key, r.project_label, r.started_utc "
|
|
727
|
+
"FROM codex_conversation_threads AS t "
|
|
728
|
+
"LEFT JOIN codex_conversation_rollups AS r "
|
|
729
|
+
"ON r.conversation_key=t.conversation_key "
|
|
730
|
+
"ORDER BY r.last_activity_utc DESC, t.conversation_key DESC"
|
|
731
|
+
))
|
|
732
|
+
file_aliases = tuple(cache_conn.execute(
|
|
733
|
+
"SELECT source_root_key, path, last_native_thread_id, last_session_id "
|
|
734
|
+
"FROM codex_session_files "
|
|
735
|
+
"WHERE last_native_thread_id IS NOT NULL AND last_native_thread_id != '' "
|
|
736
|
+
"ORDER BY last_ingested_at DESC, path DESC"
|
|
737
|
+
))
|
|
738
|
+
native_ids = tuple(sorted({
|
|
739
|
+
str(native_thread_id) for _, _, native_thread_id, *_ in rows
|
|
740
|
+
if isinstance(native_thread_id, str) and native_thread_id
|
|
741
|
+
} | {
|
|
742
|
+
str(native_thread_id) for _, _, native_thread_id, _ in file_aliases
|
|
743
|
+
if isinstance(native_thread_id, str) and native_thread_id
|
|
744
|
+
}))
|
|
745
|
+
provider_roots = {
|
|
746
|
+
str(root_key): pathlib.Path(root_path)
|
|
747
|
+
for root_key, root_path in cache_conn.execute(
|
|
748
|
+
"SELECT source_root_key, canonical_root_path FROM codex_source_roots "
|
|
749
|
+
"ORDER BY source_root_key"
|
|
750
|
+
)
|
|
751
|
+
if isinstance(root_key, str) and root_key
|
|
752
|
+
and isinstance(root_path, str) and root_path
|
|
753
|
+
}
|
|
754
|
+
short_names: dict[str, str] = {}
|
|
755
|
+
for provider_root in provider_roots.values():
|
|
756
|
+
state_path = provider_root / "state_5.sqlite"
|
|
757
|
+
if not state_path.is_file():
|
|
758
|
+
continue
|
|
759
|
+
state_conn: sqlite3.Connection | None = None
|
|
760
|
+
try:
|
|
761
|
+
state_conn = sqlite3.connect(
|
|
762
|
+
f"{state_path.resolve().as_uri()}?mode=ro",
|
|
763
|
+
uri=True,
|
|
764
|
+
timeout=0.05,
|
|
765
|
+
)
|
|
766
|
+
for offset in range(0, len(native_ids), 500):
|
|
767
|
+
batch = native_ids[offset:offset + 500]
|
|
768
|
+
if not batch:
|
|
769
|
+
continue
|
|
770
|
+
placeholders = ",".join("?" for _ in batch)
|
|
771
|
+
for thread_id, title in state_conn.execute(
|
|
772
|
+
f"SELECT id, title FROM threads WHERE id IN ({placeholders})",
|
|
773
|
+
batch,
|
|
774
|
+
):
|
|
775
|
+
clean_title = " ".join(str(title or "").split())
|
|
776
|
+
if clean_title:
|
|
777
|
+
short_names[str(thread_id)] = clean_title
|
|
778
|
+
except (OSError, sqlite3.Error):
|
|
779
|
+
continue
|
|
780
|
+
finally:
|
|
781
|
+
if state_conn is not None:
|
|
782
|
+
state_conn.close()
|
|
783
|
+
|
|
784
|
+
metadata_by_native: dict[tuple[str, str], dict[str, object]] = {}
|
|
785
|
+
for (
|
|
786
|
+
root_key, source_path, native_thread_id, accounting_session_id,
|
|
787
|
+
project_key, project_label, started_at,
|
|
788
|
+
) in rows:
|
|
789
|
+
identity = (str(root_key or ""), str(source_path or ""))
|
|
790
|
+
if not all(identity) or identity in metadata:
|
|
791
|
+
continue
|
|
792
|
+
item = {
|
|
793
|
+
"title": short_names.get(str(native_thread_id or "")),
|
|
794
|
+
"native_thread_id": native_thread_id,
|
|
795
|
+
"accounting_session_id": accounting_session_id,
|
|
796
|
+
"root_path": str(provider_roots.get(identity[0]) or ""),
|
|
797
|
+
"project_key": project_key,
|
|
798
|
+
"project_label": project_label,
|
|
799
|
+
"started_at": started_at,
|
|
800
|
+
}
|
|
801
|
+
metadata[identity] = item
|
|
802
|
+
native_identity = (identity[0], str(native_thread_id or ""))
|
|
803
|
+
existing = metadata_by_native.get(native_identity)
|
|
804
|
+
if existing is None or (not existing.get("project_key") and project_key):
|
|
805
|
+
metadata_by_native[native_identity] = item
|
|
806
|
+
|
|
807
|
+
# A child rollout can be accounting-complete while its own historical
|
|
808
|
+
# conversation-thread row is absent (for example, a file first cached
|
|
809
|
+
# before conversation normalization was introduced). The cursor still
|
|
810
|
+
# persists the rooted native thread id. Inherit only presentation
|
|
811
|
+
# metadata from that rooted task; the child's accounting path and
|
|
812
|
+
# session id remain its own identity and totals are never merged.
|
|
813
|
+
for root_key, source_path, native_thread_id, accounting_session_id in file_aliases:
|
|
814
|
+
identity = (str(root_key or ""), str(source_path or ""))
|
|
815
|
+
if not all(identity) or identity in metadata:
|
|
816
|
+
continue
|
|
817
|
+
native_identity = (identity[0], str(native_thread_id or ""))
|
|
818
|
+
inherited = metadata_by_native.get(native_identity)
|
|
819
|
+
metadata[identity] = {
|
|
820
|
+
"title": short_names.get(native_identity[1]) or (inherited or {}).get("title"),
|
|
821
|
+
"native_thread_id": native_thread_id,
|
|
822
|
+
"accounting_session_id": accounting_session_id,
|
|
823
|
+
"root_path": str(provider_roots.get(identity[0]) or ""),
|
|
824
|
+
"project_key": (inherited or {}).get("project_key"),
|
|
825
|
+
"project_label": (inherited or {}).get("project_label"),
|
|
826
|
+
"started_at": (inherited or {}).get("started_at"),
|
|
827
|
+
}
|
|
828
|
+
except sqlite3.Error:
|
|
829
|
+
return {}
|
|
830
|
+
return metadata
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _session_wire(
|
|
834
|
+
view: Any,
|
|
835
|
+
*,
|
|
836
|
+
metadata: Mapping[tuple[str, str], Mapping[str, object]] | None = None,
|
|
837
|
+
) -> dict[str, object]:
|
|
314
838
|
rows = []
|
|
315
|
-
for
|
|
839
|
+
for row in view.rows:
|
|
316
840
|
# The Codex session aggregator intentionally splits equal relative
|
|
317
841
|
# session paths from distinct $CODEX_HOME roots. The opaque detail
|
|
318
842
|
# key must use that same grouping identity or two visible rows route
|
|
319
843
|
# to one another's detail payload.
|
|
320
844
|
root_identity = row.codex_root or "single-root"
|
|
845
|
+
row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
|
|
846
|
+
if row_metadata is None and metadata is not None:
|
|
847
|
+
row_metadata = next((
|
|
848
|
+
value for (root_key, source_path), value in metadata.items()
|
|
849
|
+
if (
|
|
850
|
+
str(value.get("native_thread_id") or "") == str(row.session_id or "")
|
|
851
|
+
or str(value.get("accounting_session_id") or "") == str(row.session_id or "")
|
|
852
|
+
or source_path == str(row.session_id_path)
|
|
853
|
+
)
|
|
854
|
+
and (
|
|
855
|
+
not row.codex_root
|
|
856
|
+
or str(row.codex_root) in (
|
|
857
|
+
root_key,
|
|
858
|
+
str(value.get("root_path") or ""),
|
|
859
|
+
)
|
|
860
|
+
)
|
|
861
|
+
), None)
|
|
862
|
+
title = str(row_metadata.get("title") or "").strip() if row_metadata else ""
|
|
863
|
+
project = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
864
|
+
started_at = row_metadata.get("started_at") if row_metadata else None
|
|
865
|
+
duration_min = None
|
|
866
|
+
if isinstance(started_at, str):
|
|
867
|
+
try:
|
|
868
|
+
started_dt = dt.datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
869
|
+
duration_min = max(0, round((row.last_activity.astimezone(UTC) - started_dt.astimezone(UTC)).total_seconds() / 60))
|
|
870
|
+
except (TypeError, ValueError):
|
|
871
|
+
started_at = None
|
|
321
872
|
rows.append({
|
|
322
873
|
"key": dashboard_resource_key(
|
|
323
874
|
"session", "codex", root_identity, row.session_id_path,
|
|
324
875
|
),
|
|
325
876
|
"source": "codex",
|
|
326
|
-
"label":
|
|
877
|
+
"label": title or None,
|
|
878
|
+
"project": project or None,
|
|
879
|
+
"project_key": row_metadata.get("project_key") if row_metadata else None,
|
|
880
|
+
"started_at": started_at,
|
|
881
|
+
"duration_min": duration_min,
|
|
327
882
|
"last_activity": row.last_activity.astimezone(UTC).isoformat(),
|
|
328
883
|
"cost_usd": row.cost_usd,
|
|
329
884
|
"input_tokens": row.input_tokens,
|
|
@@ -332,6 +887,9 @@ def _session_wire(view: Any) -> dict[str, object]:
|
|
|
332
887
|
"reasoning_output_tokens": row.reasoning_output_tokens,
|
|
333
888
|
"total_tokens": row.total_tokens,
|
|
334
889
|
"models": tuple(row.models),
|
|
890
|
+
"model_breakdowns": tuple(
|
|
891
|
+
dict(item) for item in getattr(row, "model_breakdowns", ())
|
|
892
|
+
),
|
|
335
893
|
})
|
|
336
894
|
return {
|
|
337
895
|
"rows": tuple(rows),
|
|
@@ -341,31 +899,111 @@ def _session_wire(view: Any) -> dict[str, object]:
|
|
|
341
899
|
}
|
|
342
900
|
|
|
343
901
|
|
|
344
|
-
def _quota_wire(
|
|
902
|
+
def _quota_wire(
|
|
903
|
+
stats_conn: sqlite3.Connection,
|
|
904
|
+
*,
|
|
905
|
+
accounting_entries: Iterable[object] = (),
|
|
906
|
+
cycle: CodexCycleBoundary | None = None,
|
|
907
|
+
now_utc: dt.datetime | None = None,
|
|
908
|
+
display_tz_name: str | None = None,
|
|
909
|
+
) -> tuple[dict[str, object], ...]:
|
|
910
|
+
"""Build current-cycle Codex 5-hour activity rows from durable windows.
|
|
911
|
+
|
|
912
|
+
The durable projection supplies the truthful native block boundaries. Cost,
|
|
913
|
+
tokens, and model splits come from root-qualified accounting inside each
|
|
914
|
+
half-open 300-minute interval. Weekly quota summaries are deliberately not
|
|
915
|
+
activity blocks and never enter this wire.
|
|
916
|
+
"""
|
|
917
|
+
if cycle is None or now_utc is None:
|
|
918
|
+
return ()
|
|
345
919
|
try:
|
|
346
920
|
rows = stats_conn.execute(
|
|
347
921
|
"SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, "
|
|
348
|
-
"limit_name, resets_at_utc, current_percent, orphaned_at "
|
|
349
|
-
"FROM quota_window_blocks WHERE source='codex' "
|
|
922
|
+
"limit_name, resets_at_utc, nominal_start_at_utc, current_percent, orphaned_at "
|
|
923
|
+
"FROM quota_window_blocks WHERE source='codex' AND window_minutes=300 "
|
|
350
924
|
"ORDER BY resets_at_utc DESC, source_root_key, logical_limit_key, observed_slot "
|
|
351
925
|
"LIMIT ?",
|
|
352
926
|
(SOURCE_HISTORY_LIMIT,),
|
|
353
927
|
).fetchall()
|
|
354
928
|
except sqlite3.Error:
|
|
355
929
|
return ()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"resets_at": resets_at,
|
|
363
|
-
"current_percent": current_percent,
|
|
364
|
-
"orphaned": orphaned_at is not None,
|
|
365
|
-
} for (
|
|
930
|
+
entries = tuple(accounting_entries)
|
|
931
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
932
|
+
c = sys.modules["cctally"]
|
|
933
|
+
wired: list[dict[str, object]] = []
|
|
934
|
+
seen_windows: set[tuple[str, dt.datetime, dt.datetime]] = set()
|
|
935
|
+
for (
|
|
366
936
|
root_key, logical_limit_key, observed_slot, window_minutes,
|
|
367
|
-
|
|
368
|
-
) in rows
|
|
937
|
+
_limit_name, resets_at_raw, nominal_start_raw, current_percent, orphaned_at,
|
|
938
|
+
) in rows:
|
|
939
|
+
if orphaned_at is not None or str(root_key) not in cycle.source_root_keys:
|
|
940
|
+
continue
|
|
941
|
+
try:
|
|
942
|
+
start_at = dt.datetime.fromisoformat(str(nominal_start_raw).replace("Z", "+00:00"))
|
|
943
|
+
resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
|
|
944
|
+
except (TypeError, ValueError):
|
|
945
|
+
continue
|
|
946
|
+
if start_at.tzinfo is None or resets_at.tzinfo is None:
|
|
947
|
+
continue
|
|
948
|
+
start_at = start_at.astimezone(UTC)
|
|
949
|
+
resets_at = resets_at.astimezone(UTC)
|
|
950
|
+
if resets_at <= cycle.start_at or start_at >= cycle.resets_at:
|
|
951
|
+
continue
|
|
952
|
+
physical_key = (str(root_key), start_at, resets_at)
|
|
953
|
+
if physical_key in seen_windows:
|
|
954
|
+
continue
|
|
955
|
+
seen_windows.add(physical_key)
|
|
956
|
+
block_entries = tuple(
|
|
957
|
+
entry for entry in entries
|
|
958
|
+
if str(getattr(entry, "source_root_key", "")) == str(root_key)
|
|
959
|
+
and start_at <= getattr(entry, "timestamp").astimezone(UTC) < resets_at
|
|
960
|
+
)
|
|
961
|
+
if not block_entries:
|
|
962
|
+
continue
|
|
963
|
+
by_model: dict[str, dict[str, object]] = {}
|
|
964
|
+
for entry in block_entries:
|
|
965
|
+
model = str(getattr(entry, "model", "") or "unknown")
|
|
966
|
+
aggregate = by_model.setdefault(model, {
|
|
967
|
+
"modelName": model,
|
|
968
|
+
"inputTokens": 0,
|
|
969
|
+
"cachedInputTokens": 0,
|
|
970
|
+
"outputTokens": 0,
|
|
971
|
+
"reasoningOutputTokens": 0,
|
|
972
|
+
"totalTokens": 0,
|
|
973
|
+
"costParts": [],
|
|
974
|
+
})
|
|
975
|
+
aggregate["inputTokens"] += int(getattr(entry, "input_tokens", 0))
|
|
976
|
+
aggregate["cachedInputTokens"] += int(getattr(entry, "cached_input_tokens", 0))
|
|
977
|
+
aggregate["outputTokens"] += int(getattr(entry, "output_tokens", 0))
|
|
978
|
+
aggregate["reasoningOutputTokens"] += int(getattr(entry, "reasoning_output_tokens", 0))
|
|
979
|
+
aggregate["totalTokens"] += int(getattr(entry, "total_tokens", 0))
|
|
980
|
+
aggregate["costParts"].append(float(getattr(entry, "cost_usd", 0.0)))
|
|
981
|
+
breakdowns: list[dict[str, object]] = []
|
|
982
|
+
for aggregate in by_model.values():
|
|
983
|
+
cost = stable_sum(aggregate.pop("costParts"))
|
|
984
|
+
breakdowns.append({**aggregate, "cost": cost})
|
|
985
|
+
breakdowns.sort(key=lambda row: (-float(row["cost"]), str(row["modelName"])))
|
|
986
|
+
cost_usd = stable_sum(float(row["cost"]) for row in breakdowns)
|
|
987
|
+
wired.append({
|
|
988
|
+
"key": dashboard_resource_key(
|
|
989
|
+
"block", "codex", root_key, logical_limit_key,
|
|
990
|
+
observed_slot, window_minutes, resets_at_raw,
|
|
991
|
+
),
|
|
992
|
+
"source": "codex",
|
|
993
|
+
"label": c.format_display_dt(
|
|
994
|
+
start_at, display_tz, fmt="%H:%M %b %d", suffix=True,
|
|
995
|
+
),
|
|
996
|
+
"window_minutes": window_minutes,
|
|
997
|
+
"start_at": start_at.isoformat(),
|
|
998
|
+
"end_at": resets_at.isoformat(),
|
|
999
|
+
"resets_at": resets_at_raw,
|
|
1000
|
+
"current_percent": current_percent,
|
|
1001
|
+
"orphaned": False,
|
|
1002
|
+
"is_active": start_at <= now_utc < resets_at,
|
|
1003
|
+
"cost_usd": cost_usd,
|
|
1004
|
+
"model_breakdowns": tuple(breakdowns),
|
|
1005
|
+
})
|
|
1006
|
+
return tuple(wired)
|
|
369
1007
|
|
|
370
1008
|
|
|
371
1009
|
def _budget_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
|
|
@@ -489,9 +1127,12 @@ def _configured_codex_budget_window(
|
|
|
489
1127
|
def _quota_read_model(
|
|
490
1128
|
context: DashboardReadContext,
|
|
491
1129
|
observations: Iterable[object],
|
|
1130
|
+
*,
|
|
1131
|
+
accounting_entries: Iterable[object] = (),
|
|
492
1132
|
) -> dict[str, object]:
|
|
493
1133
|
"""Use S2's pure history/block/forecast kernels over cache evidence."""
|
|
494
1134
|
quota_observations = tuple(observations)
|
|
1135
|
+
cost_entries = tuple(accounting_entries)
|
|
495
1136
|
histories = build_history(quota_observations)
|
|
496
1137
|
blocks = build_blocks(quota_observations)
|
|
497
1138
|
history_rows: list[dict[str, object]] = []
|
|
@@ -511,7 +1152,7 @@ def _quota_read_model(
|
|
|
511
1152
|
history_rows.append({
|
|
512
1153
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
513
1154
|
"source": "codex",
|
|
514
|
-
"label": identity.limit_name
|
|
1155
|
+
"label": _native_limit_label(identity.limit_name, identity.window_minutes),
|
|
515
1156
|
"observed_slot": identity.observed_slot,
|
|
516
1157
|
"window_minutes": identity.window_minutes,
|
|
517
1158
|
"current_percent": baseline.used_percent if baseline is not None else None,
|
|
@@ -551,7 +1192,82 @@ def _quota_read_model(
|
|
|
551
1192
|
identity.window_minutes,
|
|
552
1193
|
block.resets_at.astimezone(UTC).isoformat(),
|
|
553
1194
|
)
|
|
1195
|
+
quota_key = dashboard_resource_key(
|
|
1196
|
+
"quota", "codex", identity.source_root_key,
|
|
1197
|
+
identity.logical_limit_key, identity.observed_slot,
|
|
1198
|
+
identity.window_minutes,
|
|
1199
|
+
)
|
|
1200
|
+
block_cost_entries = tuple(
|
|
1201
|
+
entry for entry in cost_entries
|
|
1202
|
+
if str(getattr(entry, "source_root_key", "")) == identity.source_root_key
|
|
1203
|
+
and block.nominal_start_at
|
|
1204
|
+
<= getattr(entry, "timestamp").astimezone(UTC)
|
|
1205
|
+
< block.resets_at
|
|
1206
|
+
)
|
|
1207
|
+
canonical_rows = ()
|
|
1208
|
+
if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
|
|
1209
|
+
try:
|
|
1210
|
+
canonical_rows = codex_quota_breakdown(
|
|
1211
|
+
identity,
|
|
1212
|
+
block.resets_at,
|
|
1213
|
+
speed=context.speed,
|
|
1214
|
+
cache_conn=context.cache_conn,
|
|
1215
|
+
stats_conn=context.stats_conn,
|
|
1216
|
+
)
|
|
1217
|
+
except sqlite3.Error:
|
|
1218
|
+
# Older or partially migrated stores retain the bounded
|
|
1219
|
+
# observation-derived fallback below. A coherent current
|
|
1220
|
+
# store always has the durable projection used by the CLI.
|
|
1221
|
+
canonical_rows = ()
|
|
1222
|
+
if canonical_rows:
|
|
1223
|
+
try:
|
|
1224
|
+
correlated_five_hour = tuple(
|
|
1225
|
+
observation
|
|
1226
|
+
for observation in load_codex_quota_observations(
|
|
1227
|
+
source_root_keys={identity.source_root_key},
|
|
1228
|
+
cache_conn=context.cache_conn,
|
|
1229
|
+
captured_at_or_after=block.nominal_start_at,
|
|
1230
|
+
)
|
|
1231
|
+
if observation.identity.window_minutes == 300
|
|
1232
|
+
and observation.identity.observed_slot == identity.observed_slot
|
|
1233
|
+
and observation.identity.limit_id == identity.limit_id
|
|
1234
|
+
)
|
|
1235
|
+
except sqlite3.Error:
|
|
1236
|
+
correlated_five_hour = ()
|
|
1237
|
+
|
|
1238
|
+
for row in canonical_rows:
|
|
1239
|
+
milestone_rows.append({
|
|
1240
|
+
"key": dashboard_resource_key(
|
|
1241
|
+
"quota_milestone", "codex", *block_parts,
|
|
1242
|
+
row.percent, row.captured_at.astimezone(UTC).isoformat(),
|
|
1243
|
+
),
|
|
1244
|
+
"source": "codex",
|
|
1245
|
+
"block_key": dashboard_resource_key("block", "codex", *block_parts),
|
|
1246
|
+
"quota_key": quota_key,
|
|
1247
|
+
"window_minutes": identity.window_minutes,
|
|
1248
|
+
"resets_at": block.resets_at.astimezone(UTC).isoformat(),
|
|
1249
|
+
"percent": row.percent,
|
|
1250
|
+
"captured_at": row.captured_at.astimezone(UTC).isoformat(),
|
|
1251
|
+
"cumulative_usd": row.cost_usd,
|
|
1252
|
+
"marginal_usd": row.marginal_cost_usd,
|
|
1253
|
+
"input_tokens": row.input_tokens,
|
|
1254
|
+
"cached_input_tokens": row.cached_input_tokens,
|
|
1255
|
+
"output_tokens": row.output_tokens,
|
|
1256
|
+
"reasoning_output_tokens": row.reasoning_output_tokens,
|
|
1257
|
+
"total_tokens": row.total_tokens,
|
|
1258
|
+
"five_hour_percent": codex_five_hour_percent_at_crossing(
|
|
1259
|
+
identity, row.captured_at, correlated_five_hour,
|
|
1260
|
+
),
|
|
1261
|
+
})
|
|
1262
|
+
continue
|
|
1263
|
+
|
|
1264
|
+
previous_cumulative = 0.0
|
|
554
1265
|
for milestone in percent_milestones(block):
|
|
1266
|
+
cumulative_usd = stable_sum(
|
|
1267
|
+
float(getattr(entry, "cost_usd", 0.0))
|
|
1268
|
+
for entry in block_cost_entries
|
|
1269
|
+
if getattr(entry, "timestamp").astimezone(UTC) <= milestone.captured_at
|
|
1270
|
+
)
|
|
555
1271
|
milestone_rows.append({
|
|
556
1272
|
"key": dashboard_resource_key(
|
|
557
1273
|
"quota_milestone", "codex", *block_parts,
|
|
@@ -559,9 +1275,15 @@ def _quota_read_model(
|
|
|
559
1275
|
),
|
|
560
1276
|
"source": "codex",
|
|
561
1277
|
"block_key": dashboard_resource_key("block", "codex", *block_parts),
|
|
1278
|
+
"quota_key": quota_key,
|
|
1279
|
+
"window_minutes": identity.window_minutes,
|
|
1280
|
+
"resets_at": block.resets_at.astimezone(UTC).isoformat(),
|
|
562
1281
|
"percent": milestone.percent,
|
|
563
1282
|
"captured_at": milestone.captured_at.astimezone(UTC).isoformat(),
|
|
1283
|
+
"cumulative_usd": cumulative_usd,
|
|
1284
|
+
"marginal_usd": max(0.0, cumulative_usd - previous_cumulative),
|
|
564
1285
|
})
|
|
1286
|
+
previous_cumulative = cumulative_usd
|
|
565
1287
|
latest_percent = max(
|
|
566
1288
|
(float(row["current_percent"]) for row in active_rows), default=None,
|
|
567
1289
|
)
|
|
@@ -607,6 +1329,41 @@ def _clock_freshness(
|
|
|
607
1329
|
return "stale" if age_seconds > stale_after else "fresh"
|
|
608
1330
|
|
|
609
1331
|
|
|
1332
|
+
def _clock_cycle_validity(
|
|
1333
|
+
histories: Iterable[object],
|
|
1334
|
+
now_utc: dt.datetime,
|
|
1335
|
+
) -> tuple[bool, str]:
|
|
1336
|
+
"""Re-evaluate frozen weekly evidence without touching cache or rollouts."""
|
|
1337
|
+
boundaries: set[dt.datetime] = set()
|
|
1338
|
+
stale_weekly_evidence = False
|
|
1339
|
+
for raw_history in histories:
|
|
1340
|
+
if not isinstance(raw_history, Mapping):
|
|
1341
|
+
continue
|
|
1342
|
+
if raw_history.get("window_minutes") != 10_080:
|
|
1343
|
+
continue
|
|
1344
|
+
current = raw_history.get("current_percent")
|
|
1345
|
+
forecast = raw_history.get("forecast")
|
|
1346
|
+
if current is None or not isinstance(forecast, Mapping):
|
|
1347
|
+
continue
|
|
1348
|
+
try:
|
|
1349
|
+
resets_at = dt.datetime.fromisoformat(
|
|
1350
|
+
str(forecast.get("resets_at")).replace("Z", "+00:00")
|
|
1351
|
+
).astimezone(UTC)
|
|
1352
|
+
except (TypeError, ValueError):
|
|
1353
|
+
continue
|
|
1354
|
+
if resets_at <= now_utc:
|
|
1355
|
+
continue
|
|
1356
|
+
if raw_history.get("freshness") != "fresh":
|
|
1357
|
+
stale_weekly_evidence = True
|
|
1358
|
+
continue
|
|
1359
|
+
boundaries.add(resets_at)
|
|
1360
|
+
if len(boundaries) == 1:
|
|
1361
|
+
return True, "ok"
|
|
1362
|
+
if not boundaries and stale_weekly_evidence:
|
|
1363
|
+
return False, "stale"
|
|
1364
|
+
return False, "missing" if not boundaries else "conflicting"
|
|
1365
|
+
|
|
1366
|
+
|
|
610
1367
|
def _refresh_budget_status_clock(
|
|
611
1368
|
status: Mapping[str, object] | None,
|
|
612
1369
|
now_utc: dt.datetime,
|
|
@@ -676,6 +1433,11 @@ def refresh_codex_source_clock(
|
|
|
676
1433
|
data = dict(state.data)
|
|
677
1434
|
quota = data.get("quota")
|
|
678
1435
|
quota_changed = False
|
|
1436
|
+
cycle_changed = False
|
|
1437
|
+
capabilities = state.capabilities
|
|
1438
|
+
warnings = state.warnings
|
|
1439
|
+
availability = state.availability
|
|
1440
|
+
freshness = state.freshness
|
|
679
1441
|
if isinstance(quota, Mapping):
|
|
680
1442
|
quota = dict(quota)
|
|
681
1443
|
refreshed_histories: list[dict[str, object]] = []
|
|
@@ -757,6 +1519,40 @@ def refresh_codex_source_clock(
|
|
|
757
1519
|
quota["summary"] = summary
|
|
758
1520
|
data["quota"] = quota
|
|
759
1521
|
quota_changed = bool(refreshed_histories)
|
|
1522
|
+
hero = data.get("hero")
|
|
1523
|
+
hero_capability = state.capabilities.get("hero")
|
|
1524
|
+
if (
|
|
1525
|
+
isinstance(hero, Mapping)
|
|
1526
|
+
and isinstance(hero.get("cycle"), Mapping)
|
|
1527
|
+
and hero_capability is not None
|
|
1528
|
+
and hero_capability.status == "supported"
|
|
1529
|
+
):
|
|
1530
|
+
cycle_valid, cycle_reason = _clock_cycle_validity(refreshed_histories, now_utc)
|
|
1531
|
+
if not cycle_valid:
|
|
1532
|
+
hero = dict(hero)
|
|
1533
|
+
for field in (
|
|
1534
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
1535
|
+
"reasoning_output_tokens", "total_tokens", "cycle",
|
|
1536
|
+
):
|
|
1537
|
+
hero[field] = None
|
|
1538
|
+
data["hero"] = hero
|
|
1539
|
+
refreshed_capabilities = dict(state.capabilities)
|
|
1540
|
+
refreshed_capabilities["hero"] = CapabilityRecord(
|
|
1541
|
+
"unavailable", "missing-or-conflicting-native-cycle",
|
|
1542
|
+
)
|
|
1543
|
+
capabilities = refreshed_capabilities
|
|
1544
|
+
warnings = tuple(
|
|
1545
|
+
warning for warning in state.warnings
|
|
1546
|
+
if warning.code != "codex_cycle_unavailable"
|
|
1547
|
+
) + (SourceDashboardWarning(
|
|
1548
|
+
"codex_cycle_unavailable",
|
|
1549
|
+
"Codex native reset cycle is unavailable.",
|
|
1550
|
+
"hero",
|
|
1551
|
+
),)
|
|
1552
|
+
availability = "partial"
|
|
1553
|
+
if cycle_reason == "stale":
|
|
1554
|
+
freshness = "stale"
|
|
1555
|
+
cycle_changed = True
|
|
760
1556
|
budget_domain = data.get("budget")
|
|
761
1557
|
budget_changed = False
|
|
762
1558
|
if isinstance(budget_domain, Mapping):
|
|
@@ -778,16 +1574,16 @@ def refresh_codex_source_clock(
|
|
|
778
1574
|
hero["budget"] = refreshed_budget
|
|
779
1575
|
data["hero"] = hero
|
|
780
1576
|
budget_changed = True
|
|
781
|
-
if not (quota_changed or budget_changed):
|
|
1577
|
+
if not (quota_changed or budget_changed or cycle_changed):
|
|
782
1578
|
return state
|
|
783
1579
|
refreshed_state = SourceDashboardState(
|
|
784
1580
|
source=state.source,
|
|
785
|
-
availability=
|
|
786
|
-
freshness=
|
|
787
|
-
warnings=
|
|
1581
|
+
availability=availability,
|
|
1582
|
+
freshness=freshness,
|
|
1583
|
+
warnings=warnings,
|
|
788
1584
|
data_version=state.data_version,
|
|
789
1585
|
last_success_at=state.last_success_at,
|
|
790
|
-
capabilities=
|
|
1586
|
+
capabilities=capabilities,
|
|
791
1587
|
data=data,
|
|
792
1588
|
clock_data=state.clock_data,
|
|
793
1589
|
)
|
|
@@ -888,14 +1684,114 @@ def _projects_wire(
|
|
|
888
1684
|
}
|
|
889
1685
|
|
|
890
1686
|
|
|
891
|
-
def
|
|
892
|
-
|
|
1687
|
+
def _partial_projects_wire(
|
|
1688
|
+
entries: Iterable[object],
|
|
1689
|
+
metadata: Mapping[tuple[str, str], Mapping[str, object]],
|
|
1690
|
+
) -> dict[str, object]:
|
|
1691
|
+
"""Aggregate the qualified subset when older accounting metadata is mixed.
|
|
1692
|
+
|
|
1693
|
+
Rows without a cached conversation/project identity are omitted and remain
|
|
1694
|
+
covered by the Projects-domain warning. Valid projects stay visible; their
|
|
1695
|
+
totals never include an unqualified accounting row.
|
|
1696
|
+
"""
|
|
1697
|
+
groups: dict[tuple[str, str], dict[str, object]] = {}
|
|
1698
|
+
for entry in entries:
|
|
1699
|
+
identity = (
|
|
1700
|
+
str(getattr(entry, "source_root_key", "") or ""),
|
|
1701
|
+
str(getattr(entry, "source_path", "") or ""),
|
|
1702
|
+
)
|
|
1703
|
+
row_metadata = metadata.get(identity)
|
|
1704
|
+
project_key = str(row_metadata.get("project_key") or "").strip() if row_metadata else ""
|
|
1705
|
+
project_label = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
1706
|
+
if not project_key or not project_label:
|
|
1707
|
+
continue
|
|
1708
|
+
group_key = (identity[0], project_key)
|
|
1709
|
+
group = groups.setdefault(group_key, {
|
|
1710
|
+
"project_key": project_key,
|
|
1711
|
+
"label": project_label,
|
|
1712
|
+
"sessions": set(),
|
|
1713
|
+
"first_seen": getattr(entry, "timestamp"),
|
|
1714
|
+
"last_seen": getattr(entry, "timestamp"),
|
|
1715
|
+
"cost_usd": 0.0,
|
|
1716
|
+
"input_tokens": 0,
|
|
1717
|
+
"cached_input_tokens": 0,
|
|
1718
|
+
"output_tokens": 0,
|
|
1719
|
+
"reasoning_output_tokens": 0,
|
|
1720
|
+
"total_tokens": 0,
|
|
1721
|
+
"models": {},
|
|
1722
|
+
"session_rows": {},
|
|
1723
|
+
})
|
|
1724
|
+
timestamp = getattr(entry, "timestamp")
|
|
1725
|
+
group["first_seen"] = min(group["first_seen"], timestamp)
|
|
1726
|
+
group["last_seen"] = max(group["last_seen"], timestamp)
|
|
1727
|
+
group["sessions"].add(identity)
|
|
1728
|
+
for field in (
|
|
1729
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
1730
|
+
"reasoning_output_tokens", "total_tokens",
|
|
1731
|
+
):
|
|
1732
|
+
group[field] += getattr(entry, field)
|
|
1733
|
+
model = str(getattr(entry, "model", "") or "unknown")
|
|
1734
|
+
model_totals = group["models"].setdefault(model, {
|
|
1735
|
+
"model": model, "cost_usd": 0.0, "input_tokens": 0,
|
|
1736
|
+
"cached_input_tokens": 0, "output_tokens": 0,
|
|
1737
|
+
"reasoning_output_tokens": 0, "total_tokens": 0,
|
|
1738
|
+
})
|
|
1739
|
+
session_totals = group["session_rows"].setdefault(identity, {
|
|
1740
|
+
"label": str(row_metadata.get("title") or "Session"),
|
|
1741
|
+
"last_activity": timestamp.astimezone(UTC).isoformat(),
|
|
1742
|
+
"cost_usd": 0.0, "input_tokens": 0, "cached_input_tokens": 0,
|
|
1743
|
+
"output_tokens": 0, "reasoning_output_tokens": 0, "total_tokens": 0,
|
|
1744
|
+
})
|
|
1745
|
+
if timestamp.astimezone(UTC).isoformat() > session_totals["last_activity"]:
|
|
1746
|
+
session_totals["last_activity"] = timestamp.astimezone(UTC).isoformat()
|
|
1747
|
+
for field in (
|
|
1748
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
1749
|
+
"reasoning_output_tokens", "total_tokens",
|
|
1750
|
+
):
|
|
1751
|
+
value = getattr(entry, field)
|
|
1752
|
+
model_totals[field] += value
|
|
1753
|
+
session_totals[field] += value
|
|
1754
|
+
|
|
1755
|
+
rows = []
|
|
1756
|
+
for (root_key, _project_key), group in groups.items():
|
|
1757
|
+
rows.append({
|
|
1758
|
+
"key": dashboard_resource_key("project", "codex", root_key, group["project_key"]),
|
|
1759
|
+
"source": "codex",
|
|
1760
|
+
"label": group["label"],
|
|
1761
|
+
"session_count": len(group["sessions"]),
|
|
1762
|
+
"first_seen": group["first_seen"].astimezone(UTC).isoformat(),
|
|
1763
|
+
"last_seen": group["last_seen"].astimezone(UTC).isoformat(),
|
|
1764
|
+
"cost_usd": group["cost_usd"],
|
|
1765
|
+
"input_tokens": group["input_tokens"],
|
|
1766
|
+
"cached_input_tokens": group["cached_input_tokens"],
|
|
1767
|
+
"output_tokens": group["output_tokens"],
|
|
1768
|
+
"reasoning_output_tokens": group["reasoning_output_tokens"],
|
|
1769
|
+
"total_tokens": group["total_tokens"],
|
|
1770
|
+
"models": tuple(sorted(
|
|
1771
|
+
group["models"].values(),
|
|
1772
|
+
key=lambda item: (-float(item["cost_usd"]), str(item["model"])),
|
|
1773
|
+
)),
|
|
1774
|
+
"sessions": tuple(sorted(
|
|
1775
|
+
group["session_rows"].values(),
|
|
1776
|
+
key=lambda item: str(item["last_activity"]), reverse=True,
|
|
1777
|
+
)),
|
|
1778
|
+
})
|
|
1779
|
+
rows.sort(key=lambda row: (-float(row["cost_usd"]), str(row["label"]), str(row["key"])))
|
|
1780
|
+
return {
|
|
1781
|
+
"rows": tuple(rows),
|
|
1782
|
+
"total_cost_usd": stable_sum(float(row["cost_usd"]) for row in rows),
|
|
1783
|
+
"total_tokens": sum(int(row["total_tokens"]) for row in rows),
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
|
|
1788
|
+
"""Adapt coordinated accounting rows for the shipped non-project kernels."""
|
|
893
1789
|
converted: list[CodexEntry] = []
|
|
894
1790
|
for entry in entries:
|
|
895
1791
|
source_path = str(getattr(entry, "source_path", "") or "")
|
|
896
1792
|
session_id = str(getattr(entry, "session_id", "") or "")
|
|
897
1793
|
if not source_path or not session_id:
|
|
898
|
-
raise SourceCapabilityUnavailable("
|
|
1794
|
+
raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
|
|
899
1795
|
converted.append(CodexEntry(
|
|
900
1796
|
timestamp=getattr(entry, "timestamp"),
|
|
901
1797
|
session_id=session_id,
|
|
@@ -910,6 +1806,84 @@ def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]
|
|
|
910
1806
|
return converted
|
|
911
1807
|
|
|
912
1808
|
|
|
1809
|
+
def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]:
|
|
1810
|
+
"""Compatibility name retained for the source-detail reader."""
|
|
1811
|
+
return _codex_entries_from_accounting(entries)
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def _build_codex_native_weekly_view(
|
|
1815
|
+
stats_conn: sqlite3.Connection,
|
|
1816
|
+
entries: Iterable[object],
|
|
1817
|
+
*,
|
|
1818
|
+
source_root_keys: Iterable[str],
|
|
1819
|
+
active_cycle: CodexCycleBoundary | None,
|
|
1820
|
+
now_utc: dt.datetime,
|
|
1821
|
+
display_tz_name: str | None,
|
|
1822
|
+
speed: str,
|
|
1823
|
+
) -> CodexWeeklyView:
|
|
1824
|
+
"""Aggregate Codex cost into observed native quota-cycle segments."""
|
|
1825
|
+
periods = _codex_weekly_periods(
|
|
1826
|
+
stats_conn,
|
|
1827
|
+
source_root_keys=source_root_keys,
|
|
1828
|
+
active_cycle=active_cycle,
|
|
1829
|
+
)
|
|
1830
|
+
converted: list[CodexEntry] = []
|
|
1831
|
+
bucket_by_entry: dict[int, str] = {}
|
|
1832
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
1833
|
+
labels: dict[str, str] = {}
|
|
1834
|
+
periods_by_bucket: dict[str, CodexWeeklyPeriod] = {}
|
|
1835
|
+
for entry in entries:
|
|
1836
|
+
timestamp = getattr(entry, "timestamp").astimezone(UTC)
|
|
1837
|
+
root_key = str(getattr(entry, "source_root_key", "") or "")
|
|
1838
|
+
period = next((
|
|
1839
|
+
candidate for candidate in periods
|
|
1840
|
+
if root_key in candidate.source_root_keys
|
|
1841
|
+
and candidate.start_at <= timestamp < candidate.end_at
|
|
1842
|
+
), None)
|
|
1843
|
+
if period is None:
|
|
1844
|
+
continue
|
|
1845
|
+
converted_entry = _codex_entries_from_accounting((entry,))[0]
|
|
1846
|
+
bucket = period.start_at.isoformat()
|
|
1847
|
+
converted.append(converted_entry)
|
|
1848
|
+
bucket_by_entry[id(converted_entry)] = bucket
|
|
1849
|
+
local_start = (
|
|
1850
|
+
period.start_at.astimezone(display_tz)
|
|
1851
|
+
if display_tz is not None else period.start_at.astimezone()
|
|
1852
|
+
)
|
|
1853
|
+
labels[bucket] = local_start.strftime("%m-%d %H:%M")
|
|
1854
|
+
periods_by_bucket[bucket] = period
|
|
1855
|
+
|
|
1856
|
+
rows = _aggregate_codex_buckets(
|
|
1857
|
+
converted,
|
|
1858
|
+
key_fn=lambda entry: bucket_by_entry[id(entry)],
|
|
1859
|
+
speed=speed,
|
|
1860
|
+
)
|
|
1861
|
+
display_rows = tuple(
|
|
1862
|
+
replace(
|
|
1863
|
+
row,
|
|
1864
|
+
bucket=labels[row.bucket],
|
|
1865
|
+
period_start_at=periods_by_bucket[row.bucket].start_at,
|
|
1866
|
+
period_end_at=periods_by_bucket[row.bucket].end_at,
|
|
1867
|
+
used_pct=periods_by_bucket[row.bucket].used_percent,
|
|
1868
|
+
dollar_per_pct=(
|
|
1869
|
+
row.cost_usd / periods_by_bucket[row.bucket].used_percent
|
|
1870
|
+
if periods_by_bucket[row.bucket].used_percent is not None
|
|
1871
|
+
and periods_by_bucket[row.bucket].used_percent > 0
|
|
1872
|
+
else None
|
|
1873
|
+
),
|
|
1874
|
+
)
|
|
1875
|
+
for row in rows
|
|
1876
|
+
)
|
|
1877
|
+
return CodexWeeklyView(
|
|
1878
|
+
rows=display_rows,
|
|
1879
|
+
total_cost_usd=stable_sum(row.cost_usd for row in display_rows),
|
|
1880
|
+
total_tokens=sum(row.total_tokens for row in display_rows),
|
|
1881
|
+
period_start=(periods[0].start_at if periods else None),
|
|
1882
|
+
period_end=now_utc,
|
|
1883
|
+
display_tz_label=display_tz_name or str(dt.datetime.now().astimezone().tzinfo),
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
|
|
913
1887
|
def build_codex_source_state(
|
|
914
1888
|
context: DashboardReadContext,
|
|
915
1889
|
*,
|
|
@@ -938,8 +1912,7 @@ def build_codex_source_state(
|
|
|
938
1912
|
coherence = codex_projection_coherence(
|
|
939
1913
|
context,
|
|
940
1914
|
)
|
|
941
|
-
|
|
942
|
-
raise CodexProjectionIncoherent(coherence.reason)
|
|
1915
|
+
projection_incoherent = not coherence.coherent
|
|
943
1916
|
# The cache reader's established report surface treats the ``now`` instant
|
|
944
1917
|
# as inclusive. The qualified adapter is half-open, so extend only its
|
|
945
1918
|
# query/result boundary by one microsecond and keep all live budget sums
|
|
@@ -949,37 +1922,123 @@ def build_codex_source_state(
|
|
|
949
1922
|
if context.codex_budget is not None:
|
|
950
1923
|
_period, budget_start, _budget_end = _configured_codex_budget_window(context)
|
|
951
1924
|
accounting_start = min(accounting_start, budget_start)
|
|
952
|
-
|
|
953
|
-
accounting_start,
|
|
954
|
-
accounting_end,
|
|
955
|
-
speed=context.speed,
|
|
956
|
-
sync=False,
|
|
1925
|
+
health = load_codex_project_metadata_health(
|
|
957
1926
|
cache_conn=context.cache_conn,
|
|
1927
|
+
start=accounting_start,
|
|
1928
|
+
end=accounting_end,
|
|
1929
|
+
)
|
|
1930
|
+
metadata_incomplete = health.incomplete_rows > 0
|
|
1931
|
+
metadata_warning_message = (
|
|
1932
|
+
f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
|
|
1933
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
1934
|
+
if metadata_incomplete
|
|
1935
|
+
else "Codex project metadata could not be read; "
|
|
1936
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
958
1937
|
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1938
|
+
qualified_entries: tuple[object, ...] = ()
|
|
1939
|
+
if not metadata_incomplete:
|
|
1940
|
+
try:
|
|
1941
|
+
qualified_entries = load_qualified_codex_entries(
|
|
1942
|
+
accounting_start,
|
|
1943
|
+
accounting_end,
|
|
1944
|
+
speed=context.speed,
|
|
1945
|
+
sync=False,
|
|
1946
|
+
cache_conn=context.cache_conn,
|
|
1947
|
+
)
|
|
1948
|
+
accounting_entries: tuple[object, ...] = qualified_entries
|
|
1949
|
+
except QualifiedMetadataUnavailable:
|
|
1950
|
+
# A cached read must be internally coherent, but retain accounting
|
|
1951
|
+
# once if a defensive race or malformed row violates that premise.
|
|
1952
|
+
_lib_log.get_logger("dashboard").warning(
|
|
1953
|
+
"Codex qualified metadata read became unavailable; using cache-only accounting fallback"
|
|
1954
|
+
)
|
|
1955
|
+
metadata_incomplete = True
|
|
1956
|
+
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
1957
|
+
accounting_start,
|
|
1958
|
+
accounting_end,
|
|
1959
|
+
speed=context.speed,
|
|
1960
|
+
cache_conn=context.cache_conn,
|
|
1961
|
+
)
|
|
1962
|
+
else:
|
|
1963
|
+
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
1964
|
+
accounting_start,
|
|
1965
|
+
accounting_end,
|
|
1966
|
+
speed=context.speed,
|
|
1967
|
+
cache_conn=context.cache_conn,
|
|
1968
|
+
)
|
|
1969
|
+
budget_entries = _codex_entries_from_accounting(accounting_entries)
|
|
1970
|
+
cycle_reason: str | None = None
|
|
1971
|
+
try:
|
|
1972
|
+
cycle = _resolve_codex_weekly_cycle(quota_observations, context.now_utc)
|
|
1973
|
+
except CodexCycleUnavailable as exc:
|
|
1974
|
+
cycle = None
|
|
1975
|
+
cycle_reason = exc.reason
|
|
1976
|
+
cycle_failure = cycle is None and has_cached_codex_accounting_entries(
|
|
1977
|
+
cache_conn=context.cache_conn,
|
|
1978
|
+
)
|
|
1979
|
+
hero_failure = projection_incoherent or cycle_failure
|
|
1980
|
+
if cycle is None or hero_failure:
|
|
1981
|
+
cycle_entries: list[CodexEntry] = []
|
|
1982
|
+
cycle_cost_usd: float | None = None if hero_failure else 0.0
|
|
1983
|
+
else:
|
|
1984
|
+
cycle_end = min(accounting_end, cycle.resets_at)
|
|
1985
|
+
cycle_rows = load_cached_rooted_codex_accounting_entries(
|
|
1986
|
+
cycle.start_at,
|
|
1987
|
+
cycle_end,
|
|
1988
|
+
speed=context.speed,
|
|
1989
|
+
cache_conn=context.cache_conn,
|
|
1990
|
+
source_root_keys=cycle.source_root_keys,
|
|
1991
|
+
)
|
|
1992
|
+
cycle_entries = _codex_entries_from_accounting(cycle_rows)
|
|
1993
|
+
cycle_cost_usd = build_codex_daily_view(
|
|
1994
|
+
cycle_entries,
|
|
1995
|
+
now_utc=context.now_utc,
|
|
1996
|
+
tz_name=context.display_tz_name,
|
|
1997
|
+
speed=context.speed,
|
|
1998
|
+
).total_cost_usd
|
|
1999
|
+
visible_accounting_entries = tuple(
|
|
2000
|
+
entry for entry in accounting_entries
|
|
962
2001
|
if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
|
|
963
2002
|
)
|
|
964
|
-
entries =
|
|
2003
|
+
entries = _codex_entries_from_accounting(visible_accounting_entries)
|
|
965
2004
|
daily = build_codex_daily_view(
|
|
966
2005
|
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
967
2006
|
)
|
|
968
2007
|
monthly = build_codex_monthly_view(
|
|
969
2008
|
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
970
2009
|
)
|
|
971
|
-
weekly =
|
|
972
|
-
|
|
2010
|
+
weekly = _build_codex_native_weekly_view(
|
|
2011
|
+
context.stats_conn,
|
|
2012
|
+
visible_accounting_entries,
|
|
2013
|
+
source_root_keys=active_roots,
|
|
2014
|
+
active_cycle=cycle,
|
|
973
2015
|
now_utc=context.now_utc,
|
|
974
|
-
|
|
975
|
-
week_start_idx=context.week_start_idx,
|
|
2016
|
+
display_tz_name=context.display_tz_name,
|
|
976
2017
|
speed=context.speed,
|
|
977
2018
|
)
|
|
978
|
-
sessions =
|
|
979
|
-
|
|
2019
|
+
sessions = (
|
|
2020
|
+
build_rooted_codex_session_view(
|
|
2021
|
+
visible_accounting_entries,
|
|
2022
|
+
now_utc=context.now_utc,
|
|
2023
|
+
tz_name=context.display_tz_name,
|
|
2024
|
+
speed=context.speed,
|
|
2025
|
+
)
|
|
2026
|
+
if metadata_incomplete else build_codex_session_view(
|
|
2027
|
+
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
2028
|
+
)
|
|
2029
|
+
)
|
|
2030
|
+
quota = _quota_read_model(
|
|
2031
|
+
context,
|
|
2032
|
+
quota_observations,
|
|
2033
|
+
accounting_entries=visible_accounting_entries,
|
|
2034
|
+
)
|
|
2035
|
+
quota_blocks = _quota_wire(
|
|
2036
|
+
context.stats_conn,
|
|
2037
|
+
accounting_entries=visible_accounting_entries,
|
|
2038
|
+
cycle=cycle,
|
|
2039
|
+
now_utc=context.now_utc,
|
|
2040
|
+
display_tz_name=context.display_tz_name,
|
|
980
2041
|
)
|
|
981
|
-
quota = _quota_read_model(context, quota_observations)
|
|
982
|
-
quota_blocks = _quota_wire(context.stats_conn)
|
|
983
2042
|
quota = {**quota, "blocks": quota_blocks}
|
|
984
2043
|
budget_rows = _budget_wire(context.stats_conn)
|
|
985
2044
|
projected_budget_rows = _projected_budget_wire(context.stats_conn)
|
|
@@ -987,45 +2046,102 @@ def build_codex_source_state(
|
|
|
987
2046
|
configured_budget = _configured_codex_budget_status(
|
|
988
2047
|
context, budget_entries, cost_events=budget_cost_events,
|
|
989
2048
|
)
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
2049
|
+
conversation_metadata = _codex_conversation_metadata(context.cache_conn)
|
|
2050
|
+
cache_report = _codex_cache_report_wire(
|
|
2051
|
+
visible_accounting_entries,
|
|
2052
|
+
metadata=conversation_metadata,
|
|
2053
|
+
now_utc=context.now_utc,
|
|
2054
|
+
display_tz_name=context.display_tz_name,
|
|
2055
|
+
speed=context.speed,
|
|
2056
|
+
anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
|
|
2057
|
+
)
|
|
2058
|
+
projects = (
|
|
2059
|
+
_partial_projects_wire(visible_accounting_entries, conversation_metadata)
|
|
2060
|
+
if metadata_incomplete else _projects_wire(
|
|
2061
|
+
context,
|
|
2062
|
+
quota_observations,
|
|
2063
|
+
visible_accounting_entries,
|
|
2064
|
+
accounting_end=accounting_end,
|
|
2065
|
+
)
|
|
995
2066
|
)
|
|
996
2067
|
alerts = _alerts_wire(context.stats_conn)
|
|
997
|
-
availability =
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
2068
|
+
availability = (
|
|
2069
|
+
"partial" if metadata_incomplete or hero_failure
|
|
2070
|
+
else ("ok" if (entries or quota_blocks or budget_rows) else "empty")
|
|
2071
|
+
)
|
|
2072
|
+
hero_input = None if hero_failure else sum(entry.input_tokens for entry in cycle_entries)
|
|
2073
|
+
hero_cached = None if hero_failure else sum(entry.cached_input_tokens for entry in cycle_entries)
|
|
2074
|
+
hero_output = None if hero_failure else sum(entry.output_tokens for entry in cycle_entries)
|
|
2075
|
+
hero_reasoning = None if hero_failure else sum(entry.reasoning_output_tokens for entry in cycle_entries)
|
|
2076
|
+
hero_total = None if hero_failure else sum(entry.total_tokens for entry in cycle_entries)
|
|
2077
|
+
warnings: list[SourceDashboardWarning] = []
|
|
2078
|
+
if metadata_incomplete:
|
|
2079
|
+
warnings.append(SourceDashboardWarning(
|
|
2080
|
+
"codex_metadata_incomplete",
|
|
2081
|
+
metadata_warning_message,
|
|
2082
|
+
"projects",
|
|
2083
|
+
))
|
|
2084
|
+
if projection_incoherent:
|
|
2085
|
+
warnings.append(SourceDashboardWarning(
|
|
2086
|
+
"codex_projection_incoherent",
|
|
2087
|
+
"Codex quota projection is unavailable.",
|
|
2088
|
+
"hero",
|
|
2089
|
+
))
|
|
2090
|
+
if cycle_failure:
|
|
2091
|
+
warnings.append(SourceDashboardWarning(
|
|
2092
|
+
"codex_cycle_unavailable",
|
|
2093
|
+
"Codex native reset cycle is unavailable.",
|
|
2094
|
+
"hero",
|
|
2095
|
+
))
|
|
1002
2096
|
return SourceDashboardState(
|
|
1003
2097
|
source="codex",
|
|
1004
2098
|
availability=availability,
|
|
1005
|
-
freshness="fresh",
|
|
1006
|
-
warnings=(),
|
|
2099
|
+
freshness=("stale" if cycle_reason == "stale" else "fresh"),
|
|
2100
|
+
warnings=tuple(warnings),
|
|
1007
2101
|
data_version=data_version,
|
|
1008
2102
|
last_success_at=context.now_utc,
|
|
1009
2103
|
capabilities={
|
|
1010
|
-
"hero":
|
|
2104
|
+
"hero": (
|
|
2105
|
+
CapabilityRecord(
|
|
2106
|
+
"unavailable",
|
|
2107
|
+
(
|
|
2108
|
+
"projection-incoherent" if projection_incoherent
|
|
2109
|
+
else "missing-or-conflicting-native-cycle"
|
|
2110
|
+
),
|
|
2111
|
+
)
|
|
2112
|
+
if hero_failure
|
|
2113
|
+
else CapabilityRecord("supported", "native-reset-cycle")
|
|
2114
|
+
),
|
|
1011
2115
|
"daily": CapabilityRecord("supported", "calendar-day"),
|
|
1012
2116
|
"monthly": CapabilityRecord("supported", "calendar-month"),
|
|
1013
|
-
"weekly": CapabilityRecord("
|
|
2117
|
+
"weekly": CapabilityRecord("derived", "native-reset-cycles"),
|
|
1014
2118
|
"sessions": CapabilityRecord("supported", "inclusive-input-tokens"),
|
|
1015
2119
|
"forensics": CapabilityRecord("supported", "inclusive-input-token-reuse"),
|
|
1016
2120
|
"quota": CapabilityRecord("derived", "native-windows"),
|
|
1017
2121
|
"budget": CapabilityRecord("supported", "calendar-period"),
|
|
1018
|
-
"projects":
|
|
2122
|
+
"projects": (
|
|
2123
|
+
CapabilityRecord("supported", "conversation-metadata-partial")
|
|
2124
|
+
if metadata_incomplete
|
|
2125
|
+
else CapabilityRecord("supported", "qualified-attribution")
|
|
2126
|
+
),
|
|
1019
2127
|
"alerts": CapabilityRecord("supported", "provider-native"),
|
|
1020
2128
|
},
|
|
1021
2129
|
data={
|
|
1022
2130
|
"hero": {
|
|
1023
|
-
"cost_usd":
|
|
1024
|
-
"input_tokens":
|
|
1025
|
-
"cached_input_tokens":
|
|
1026
|
-
"output_tokens":
|
|
1027
|
-
"reasoning_output_tokens":
|
|
1028
|
-
"total_tokens":
|
|
2131
|
+
"cost_usd": cycle_cost_usd,
|
|
2132
|
+
"input_tokens": hero_input,
|
|
2133
|
+
"cached_input_tokens": hero_cached,
|
|
2134
|
+
"output_tokens": hero_output,
|
|
2135
|
+
"reasoning_output_tokens": hero_reasoning,
|
|
2136
|
+
"total_tokens": hero_total,
|
|
2137
|
+
"cycle": (
|
|
2138
|
+
{
|
|
2139
|
+
"window_minutes": cycle.window_minutes,
|
|
2140
|
+
"start_at": cycle.start_at.astimezone(UTC).isoformat(),
|
|
2141
|
+
"resets_at": cycle.resets_at.astimezone(UTC).isoformat(),
|
|
2142
|
+
}
|
|
2143
|
+
if cycle is not None and not hero_failure else None
|
|
2144
|
+
),
|
|
1029
2145
|
"quota": quota["summary"],
|
|
1030
2146
|
"budget": configured_budget,
|
|
1031
2147
|
"alerts": {"count": len(alerts)},
|
|
@@ -1035,7 +2151,7 @@ def build_codex_source_state(
|
|
|
1035
2151
|
"monthly": _period_wire(monthly),
|
|
1036
2152
|
"weekly": _period_wire(weekly),
|
|
1037
2153
|
},
|
|
1038
|
-
"sessions": _session_wire(sessions),
|
|
2154
|
+
"sessions": _session_wire(sessions, metadata=conversation_metadata),
|
|
1039
2155
|
"quota": quota,
|
|
1040
2156
|
"budget": {
|
|
1041
2157
|
"status": configured_budget,
|
|
@@ -1043,7 +2159,12 @@ def build_codex_source_state(
|
|
|
1043
2159
|
"projected": projected_budget_rows,
|
|
1044
2160
|
},
|
|
1045
2161
|
"projects": projects,
|
|
1046
|
-
"alerts": {
|
|
2162
|
+
"alerts": {
|
|
2163
|
+
"rows": alerts,
|
|
2164
|
+
"actual_thresholds": context.codex_quota_actual_thresholds,
|
|
2165
|
+
"projected_thresholds": context.codex_quota_projected_thresholds,
|
|
2166
|
+
},
|
|
2167
|
+
"cache_report": cache_report,
|
|
1047
2168
|
},
|
|
1048
2169
|
clock_data={"codex_budget_cost_events": budget_cost_events},
|
|
1049
2170
|
)
|