cctally 1.69.0 → 1.69.2
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 +10 -0
- package/bin/_cctally_dashboard_sources.py +1049 -0
- package/bin/_cctally_source_analytics.py +2050 -0
- package/bin/_lib_dashboard_sources.py +504 -0
- package/bin/_lib_source_analytics.py +1375 -0
- package/package.json +5 -1
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
"""Pure source-aware accounting contracts for the #294 S3 adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import hashlib
|
|
6
|
+
import math
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from typing import Generic, Iterable, Literal, TypeVar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Availability = Literal["ok", "empty", "partial", "unavailable"]
|
|
13
|
+
_T = TypeVar("_T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class SourceWarning:
|
|
18
|
+
"""A privacy-safe, source-scoped degradation diagnostic."""
|
|
19
|
+
|
|
20
|
+
code: str
|
|
21
|
+
message: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SourceResult(Generic[_T]):
|
|
26
|
+
"""One provider result without collapsing provider-native state."""
|
|
27
|
+
|
|
28
|
+
source: str
|
|
29
|
+
status: Availability
|
|
30
|
+
data: _T
|
|
31
|
+
warnings: tuple[SourceWarning, ...] = ()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
QUALIFIED_METADATA_WARNING = SourceWarning(
|
|
35
|
+
"qualified_metadata_unavailable",
|
|
36
|
+
"Codex qualified project metadata is unavailable.",
|
|
37
|
+
)
|
|
38
|
+
QUOTA_STATE_WARNING = SourceWarning(
|
|
39
|
+
"quota_state_unavailable",
|
|
40
|
+
"Codex quota state is unavailable.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class QualifiedCodexEntry:
|
|
46
|
+
"""One accounting row joined to its S1-qualified project identity."""
|
|
47
|
+
|
|
48
|
+
timestamp: dt.datetime
|
|
49
|
+
source_root_key: str
|
|
50
|
+
conversation_key: str
|
|
51
|
+
project_key: str
|
|
52
|
+
project_label: str
|
|
53
|
+
model: str
|
|
54
|
+
input_tokens: int
|
|
55
|
+
cached_input_tokens: int
|
|
56
|
+
output_tokens: int
|
|
57
|
+
reasoning_output_tokens: int
|
|
58
|
+
total_tokens: int
|
|
59
|
+
cost_usd: float
|
|
60
|
+
# The raw label remains available for ambiguity diagnostics. The emitted
|
|
61
|
+
# label is assigned once over the complete qualified population before any
|
|
62
|
+
# project/model filtering, so a rendered ``label (N)`` is selectable.
|
|
63
|
+
display_label: str | None = None
|
|
64
|
+
# Dashboard source read models reuse this same qualified accounting stream
|
|
65
|
+
# for the shipped Codex period/session kernels. These are internal-only;
|
|
66
|
+
# source-analytics renderers never expose either raw identity.
|
|
67
|
+
session_id: str = ""
|
|
68
|
+
source_path: str = ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def emitted_project_label(entry: QualifiedCodexEntry) -> str:
|
|
72
|
+
"""Return the deterministic privacy-safe label exposed to users."""
|
|
73
|
+
return entry.display_label or entry.project_label
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def assign_collision_safe_project_labels(
|
|
77
|
+
entries: Iterable[QualifiedCodexEntry],
|
|
78
|
+
) -> tuple[QualifiedCodexEntry, ...]:
|
|
79
|
+
"""Annotate a complete qualified population with stable display labels.
|
|
80
|
+
|
|
81
|
+
Labels are deterministic by opaque project key. Keeping the annotation on
|
|
82
|
+
the entry (rather than adding it while rendering rows) gives project
|
|
83
|
+
selection the exact string that terminal, JSON, and share emit.
|
|
84
|
+
"""
|
|
85
|
+
values = tuple(entries)
|
|
86
|
+
keys_by_label: dict[str, set[str]] = defaultdict(set)
|
|
87
|
+
for entry in values:
|
|
88
|
+
keys_by_label[entry.project_label].add(entry.project_key)
|
|
89
|
+
# Never let an allocator-owned ``label (N)`` shadow a literal project
|
|
90
|
+
# label. Reserving every raw label also makes a later presentation pass
|
|
91
|
+
# unnecessary, which avoids producing ``label (N) (N)``.
|
|
92
|
+
reserved_labels = set(keys_by_label)
|
|
93
|
+
used_labels: set[str] = set()
|
|
94
|
+
assigned: dict[str, str] = {}
|
|
95
|
+
for label in sorted(keys_by_label):
|
|
96
|
+
keys = sorted(keys_by_label[label])
|
|
97
|
+
if len(keys) == 1:
|
|
98
|
+
assigned[keys[0]] = label
|
|
99
|
+
used_labels.add(label)
|
|
100
|
+
continue
|
|
101
|
+
ordinal = 1
|
|
102
|
+
for key in keys:
|
|
103
|
+
candidate = f"{label} ({ordinal})"
|
|
104
|
+
while candidate in reserved_labels or candidate in used_labels:
|
|
105
|
+
ordinal += 1
|
|
106
|
+
candidate = f"{label} ({ordinal})"
|
|
107
|
+
assigned[key] = candidate
|
|
108
|
+
used_labels.add(candidate)
|
|
109
|
+
ordinal += 1
|
|
110
|
+
return tuple(
|
|
111
|
+
replace(entry, display_label=assigned.get(entry.project_key, entry.project_label))
|
|
112
|
+
for entry in values
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def opaque_project_key(source: str, root_key: str, resolved_key: str) -> str:
|
|
117
|
+
"""Return the stable opaque identity for one provider-qualified project."""
|
|
118
|
+
if not all(isinstance(value, str) and value for value in (source, root_key, resolved_key)):
|
|
119
|
+
raise ValueError("source, root_key, and resolved_key must be non-empty strings")
|
|
120
|
+
payload = f"{source}\0{root_key}\0{resolved_key}".encode("utf-8")
|
|
121
|
+
return "project:" + hashlib.sha256(payload).hexdigest()[:24]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
UTC = dt.timezone.utc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _require_aware(value: dt.datetime, name: str) -> dt.datetime:
|
|
128
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
129
|
+
raise ValueError(f"{name} must be timezone-aware")
|
|
130
|
+
return value.astimezone(UTC)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _opaque_key(kind: str, *parts: str) -> str:
|
|
134
|
+
if not kind or any(not isinstance(part, str) or not part for part in parts):
|
|
135
|
+
raise ValueError("opaque key parts must be non-empty strings")
|
|
136
|
+
digest = hashlib.sha256("\0".join((kind, *parts)).encode("utf-8")).hexdigest()[:24]
|
|
137
|
+
return f"{kind}:{digest}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def opaque_quota_key(
|
|
141
|
+
source: str, source_root_key: str, logical_limit_key: str,
|
|
142
|
+
observed_slot: str, window_minutes: int,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Return an opaque full-native-identity key for a quota series."""
|
|
145
|
+
if not isinstance(window_minutes, int) or isinstance(window_minutes, bool) or window_minutes <= 0:
|
|
146
|
+
raise ValueError("window_minutes must be a positive integer")
|
|
147
|
+
return _opaque_key(
|
|
148
|
+
"quota", source, source_root_key, logical_limit_key, observed_slot,
|
|
149
|
+
str(window_minutes),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class TokenTotals:
|
|
155
|
+
"""Explicit Codex-native accounting totals.
|
|
156
|
+
|
|
157
|
+
Codex input is inclusive of cache and output is inclusive of reasoning.
|
|
158
|
+
The separate fields make that vocabulary visible instead of inventing a
|
|
159
|
+
Claude cache-hit analogue.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
input_tokens: int = 0
|
|
163
|
+
cached_input_tokens: int = 0
|
|
164
|
+
output_tokens: int = 0
|
|
165
|
+
reasoning_output_tokens: int = 0
|
|
166
|
+
total_tokens: int = 0
|
|
167
|
+
cost_usd: float = 0.0
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def non_cached_input_tokens(self) -> int:
|
|
171
|
+
return max(0, self.input_tokens - self.cached_input_tokens)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def cached_input_percent(self) -> float | None:
|
|
175
|
+
if self.input_tokens <= 0:
|
|
176
|
+
return None
|
|
177
|
+
return self.cached_input_tokens * 100.0 / self.input_tokens
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _totals(entries: Iterable[QualifiedCodexEntry]) -> TokenTotals:
|
|
181
|
+
values = assign_collision_safe_project_labels(entries)
|
|
182
|
+
return TokenTotals(
|
|
183
|
+
input_tokens=sum(entry.input_tokens for entry in values),
|
|
184
|
+
cached_input_tokens=sum(entry.cached_input_tokens for entry in values),
|
|
185
|
+
output_tokens=sum(entry.output_tokens for entry in values),
|
|
186
|
+
reasoning_output_tokens=sum(entry.reasoning_output_tokens for entry in values),
|
|
187
|
+
total_tokens=sum(entry.total_tokens for entry in values),
|
|
188
|
+
cost_usd=math.fsum(entry.cost_usd for entry in values),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _codex_tokens_wire(totals: TokenTotals, *, cached_percent: bool = False) -> dict[str, object]:
|
|
193
|
+
result: dict[str, object] = {
|
|
194
|
+
"inputTokens": totals.input_tokens,
|
|
195
|
+
"cachedInputTokens": totals.cached_input_tokens,
|
|
196
|
+
"nonCachedInputTokens": totals.non_cached_input_tokens,
|
|
197
|
+
"outputTokens": totals.output_tokens,
|
|
198
|
+
"reasoningOutputTokens": totals.reasoning_output_tokens,
|
|
199
|
+
"totalTokens": totals.total_tokens,
|
|
200
|
+
}
|
|
201
|
+
if cached_percent:
|
|
202
|
+
result["cachedInputPercent"] = totals.cached_input_percent
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _metric_wire(totals: TokenTotals) -> dict[str, object]:
|
|
207
|
+
return {
|
|
208
|
+
"cost_usd": totals.cost_usd,
|
|
209
|
+
"total_tokens": totals.total_tokens,
|
|
210
|
+
"input_tokens": totals.input_tokens,
|
|
211
|
+
"cached_input_tokens": totals.cached_input_tokens,
|
|
212
|
+
"output_tokens": totals.output_tokens,
|
|
213
|
+
"reasoning_output_tokens": totals.reasoning_output_tokens,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class AnalyticsWindow:
|
|
219
|
+
"""One resolved absolute analytics interval; its end is exclusive."""
|
|
220
|
+
|
|
221
|
+
label: str
|
|
222
|
+
kind: str
|
|
223
|
+
start_at: dt.datetime
|
|
224
|
+
end_at: dt.datetime
|
|
225
|
+
used_pct_mode: str | None = None
|
|
226
|
+
|
|
227
|
+
def __post_init__(self) -> None:
|
|
228
|
+
start_at = _require_aware(self.start_at, "start_at")
|
|
229
|
+
end_at = _require_aware(self.end_at, "end_at")
|
|
230
|
+
if end_at <= start_at:
|
|
231
|
+
raise ValueError("window end_at must be after start_at")
|
|
232
|
+
object.__setattr__(self, "start_at", start_at)
|
|
233
|
+
object.__setattr__(self, "end_at", end_at)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class QuotaAttribution:
|
|
238
|
+
quota_key: str | None
|
|
239
|
+
slot: str | None
|
|
240
|
+
window_minutes: int | None
|
|
241
|
+
reset_at: dt.datetime | None
|
|
242
|
+
used_percent: float | None
|
|
243
|
+
attributed_used_percent: float | None
|
|
244
|
+
cost_per_percent: float | None
|
|
245
|
+
status: str = "ok"
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def unavailable(cls) -> "QuotaAttribution":
|
|
249
|
+
"""Represent unavailable S2 attribution without inventing identity."""
|
|
250
|
+
return cls(None, None, None, None, None, None, None, "unavailable")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass(frozen=True)
|
|
254
|
+
class CodexProjectRow:
|
|
255
|
+
project_key: str
|
|
256
|
+
display_label: str
|
|
257
|
+
session_count: int
|
|
258
|
+
first_seen: dt.datetime
|
|
259
|
+
last_seen: dt.datetime
|
|
260
|
+
totals: TokenTotals
|
|
261
|
+
models: tuple[tuple[str, TokenTotals], ...]
|
|
262
|
+
quota_attributions: tuple[QuotaAttribution, ...]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(frozen=True)
|
|
266
|
+
class CodexProjectData:
|
|
267
|
+
range_start: dt.datetime
|
|
268
|
+
range_end: dt.datetime
|
|
269
|
+
window_kind: str
|
|
270
|
+
totals: TokenTotals
|
|
271
|
+
projects: tuple[CodexProjectRow, ...]
|
|
272
|
+
include_breakdown: bool = False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _entry_in_half_open(entry: QualifiedCodexEntry, start: dt.datetime, end: dt.datetime) -> bool:
|
|
276
|
+
timestamp = _require_aware(entry.timestamp, "entry timestamp")
|
|
277
|
+
return start <= timestamp < end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_codex_project_result(
|
|
281
|
+
entries: Iterable[QualifiedCodexEntry], *, range_start: dt.datetime,
|
|
282
|
+
range_end: dt.datetime, blocks: Iterable[object] = (), as_of: dt.datetime | None = None,
|
|
283
|
+
window_kind: str = "calendar", sort: str = "cost", order: str = "desc",
|
|
284
|
+
include_breakdown: bool = False, allocation_entries: Iterable[QualifiedCodexEntry] | None = None,
|
|
285
|
+
quota_available: bool = True,
|
|
286
|
+
) -> SourceResult[CodexProjectData]:
|
|
287
|
+
"""Group qualified Codex accounting without merging provider-root identity.
|
|
288
|
+
|
|
289
|
+
Quota usage is allocated per physical root-qualified block by source-native
|
|
290
|
+
cost only. Each block remains a child attribution, rather than becoming a
|
|
291
|
+
fabricated singular Codex subscription percentage.
|
|
292
|
+
"""
|
|
293
|
+
start = _require_aware(range_start, "range_start")
|
|
294
|
+
end = _require_aware(range_end, "range_end")
|
|
295
|
+
if end <= start:
|
|
296
|
+
raise ValueError("range_end must be after range_start")
|
|
297
|
+
as_of_utc = _require_aware(as_of or end, "as_of")
|
|
298
|
+
all_entries = tuple(entries)
|
|
299
|
+
# Command adapters attach labels before filtering. Preserve that complete
|
|
300
|
+
# population metadata, while keeping the pure builder collision-safe for
|
|
301
|
+
# direct callers that supply only raw labels.
|
|
302
|
+
if all(entry.display_label is None for entry in all_entries):
|
|
303
|
+
all_entries = assign_collision_safe_project_labels(all_entries)
|
|
304
|
+
physical_population = (
|
|
305
|
+
all_entries if allocation_entries is None else tuple(allocation_entries)
|
|
306
|
+
)
|
|
307
|
+
selected = tuple(entry for entry in all_entries if _entry_in_half_open(entry, start, end))
|
|
308
|
+
grouped: dict[tuple[str, str], list[QualifiedCodexEntry]] = defaultdict(list)
|
|
309
|
+
for entry in selected:
|
|
310
|
+
grouped[(entry.source_root_key, entry.project_key)].append(entry)
|
|
311
|
+
|
|
312
|
+
attributions: dict[tuple[str, str], list[QuotaAttribution]] = defaultdict(list)
|
|
313
|
+
for block in blocks if quota_available else ():
|
|
314
|
+
identity = block.identity
|
|
315
|
+
if getattr(identity, "source", None) != "codex":
|
|
316
|
+
continue
|
|
317
|
+
nominal_start = _require_aware(block.nominal_start_at, "block nominal_start_at")
|
|
318
|
+
reset_at = _require_aware(block.resets_at, "block resets_at")
|
|
319
|
+
block_end = min(as_of_utc, reset_at)
|
|
320
|
+
if block_end <= nominal_start:
|
|
321
|
+
continue
|
|
322
|
+
block_entries = tuple(
|
|
323
|
+
entry for entry in physical_population
|
|
324
|
+
if entry.source_root_key == identity.source_root_key
|
|
325
|
+
and _entry_in_half_open(entry, nominal_start, block_end)
|
|
326
|
+
)
|
|
327
|
+
block_totals = _totals(block_entries)
|
|
328
|
+
current_percent = float(block.current_percent)
|
|
329
|
+
can_allocate = block_totals.cost_usd > 0 and current_percent > 0
|
|
330
|
+
quota_key = opaque_quota_key(
|
|
331
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
332
|
+
identity.observed_slot, identity.window_minutes,
|
|
333
|
+
)
|
|
334
|
+
for (root_key, project_key), project_entries in grouped.items():
|
|
335
|
+
if root_key != identity.source_root_key:
|
|
336
|
+
continue
|
|
337
|
+
project_cost = math.fsum(
|
|
338
|
+
entry.cost_usd for entry in project_entries
|
|
339
|
+
if _entry_in_half_open(entry, nominal_start, block_end)
|
|
340
|
+
)
|
|
341
|
+
if project_cost <= 0:
|
|
342
|
+
continue
|
|
343
|
+
attributions[(root_key, project_key)].append(QuotaAttribution(
|
|
344
|
+
quota_key=quota_key,
|
|
345
|
+
slot=identity.observed_slot,
|
|
346
|
+
window_minutes=identity.window_minutes,
|
|
347
|
+
reset_at=reset_at,
|
|
348
|
+
used_percent=current_percent,
|
|
349
|
+
attributed_used_percent=(
|
|
350
|
+
current_percent * project_cost / block_totals.cost_usd
|
|
351
|
+
if can_allocate else None
|
|
352
|
+
),
|
|
353
|
+
cost_per_percent=(
|
|
354
|
+
block_totals.cost_usd / current_percent if can_allocate else None
|
|
355
|
+
),
|
|
356
|
+
))
|
|
357
|
+
|
|
358
|
+
rows: list[CodexProjectRow] = []
|
|
359
|
+
for (root_key, project_key), project_entries in grouped.items():
|
|
360
|
+
ordered = tuple(sorted(project_entries, key=lambda entry: entry.timestamp))
|
|
361
|
+
by_model: dict[str, list[QualifiedCodexEntry]] = defaultdict(list)
|
|
362
|
+
for entry in ordered:
|
|
363
|
+
by_model[entry.model].append(entry)
|
|
364
|
+
rows.append(CodexProjectRow(
|
|
365
|
+
project_key=project_key,
|
|
366
|
+
display_label=emitted_project_label(ordered[0]),
|
|
367
|
+
session_count=len({entry.conversation_key for entry in ordered}),
|
|
368
|
+
first_seen=_require_aware(ordered[0].timestamp, "entry timestamp"),
|
|
369
|
+
last_seen=_require_aware(ordered[-1].timestamp, "entry timestamp"),
|
|
370
|
+
totals=_totals(ordered),
|
|
371
|
+
models=tuple((model, _totals(by_model[model])) for model in sorted(by_model)),
|
|
372
|
+
quota_attributions=(
|
|
373
|
+
tuple(attributions[(root_key, project_key)])
|
|
374
|
+
if quota_available else (QuotaAttribution.unavailable(),)
|
|
375
|
+
),
|
|
376
|
+
))
|
|
377
|
+
if sort not in {"cost", "name", "last-seen"}:
|
|
378
|
+
raise ValueError("Codex project sort must be cost, name, or last-seen")
|
|
379
|
+
if order not in {"asc", "desc"}:
|
|
380
|
+
raise ValueError("Codex project order must be asc or desc")
|
|
381
|
+
if sort == "name":
|
|
382
|
+
rows.sort(
|
|
383
|
+
key=lambda row: (row.display_label.lower(), row.project_key),
|
|
384
|
+
reverse=(order == "desc"),
|
|
385
|
+
)
|
|
386
|
+
elif sort == "last-seen":
|
|
387
|
+
rows.sort(
|
|
388
|
+
key=lambda row: (row.last_seen, row.display_label, row.project_key),
|
|
389
|
+
reverse=(order == "desc"),
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
rows.sort(
|
|
393
|
+
key=lambda row: (row.totals.cost_usd, row.display_label, row.project_key),
|
|
394
|
+
reverse=(order == "desc"),
|
|
395
|
+
)
|
|
396
|
+
data = CodexProjectData(
|
|
397
|
+
start, end, window_kind, _totals(selected), tuple(rows), include_breakdown,
|
|
398
|
+
)
|
|
399
|
+
status: Availability = "empty" if not selected else ("ok" if quota_available else "partial")
|
|
400
|
+
warnings = () if quota_available else (QUOTA_STATE_WARNING,)
|
|
401
|
+
return SourceResult("codex", status, data, warnings)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(frozen=True)
|
|
405
|
+
class CodexRangeData:
|
|
406
|
+
start: dt.datetime
|
|
407
|
+
end: dt.datetime
|
|
408
|
+
totals: TokenTotals
|
|
409
|
+
models: tuple[tuple[str, TokenTotals], ...]
|
|
410
|
+
include_breakdown: bool = False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def build_codex_range_result(
|
|
414
|
+
entries: Iterable[QualifiedCodexEntry], start: dt.datetime, end: dt.datetime,
|
|
415
|
+
*, include_breakdown: bool = False,
|
|
416
|
+
) -> SourceResult[CodexRangeData]:
|
|
417
|
+
"""Build the inclusive-end range-cost result for Codex accounting."""
|
|
418
|
+
start_utc = _require_aware(start, "start")
|
|
419
|
+
end_utc = _require_aware(end, "end")
|
|
420
|
+
if end_utc < start_utc:
|
|
421
|
+
raise ValueError("end must not be before start")
|
|
422
|
+
selected = tuple(
|
|
423
|
+
entry for entry in entries
|
|
424
|
+
if start_utc <= _require_aware(entry.timestamp, "entry timestamp") <= end_utc
|
|
425
|
+
)
|
|
426
|
+
by_model: dict[str, list[QualifiedCodexEntry]] = defaultdict(list)
|
|
427
|
+
for entry in selected:
|
|
428
|
+
by_model[entry.model].append(entry)
|
|
429
|
+
models = tuple((model, _totals(by_model[model])) for model in sorted(by_model))
|
|
430
|
+
data = CodexRangeData(
|
|
431
|
+
start_utc, end_utc, _totals(selected), models, include_breakdown,
|
|
432
|
+
)
|
|
433
|
+
return SourceResult("codex", "empty" if not selected else "ok", data)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@dataclass(frozen=True)
|
|
437
|
+
class ReuseRow:
|
|
438
|
+
key: str
|
|
439
|
+
label: str
|
|
440
|
+
project_key: str | None
|
|
441
|
+
project_label: str | None
|
|
442
|
+
last_activity: dt.datetime
|
|
443
|
+
totals: TokenTotals
|
|
444
|
+
models: tuple[tuple[str, TokenTotals], ...]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass(frozen=True)
|
|
448
|
+
class CodexReuseData:
|
|
449
|
+
start: dt.datetime | None
|
|
450
|
+
end: dt.datetime | None
|
|
451
|
+
group_by: str
|
|
452
|
+
totals: TokenTotals
|
|
453
|
+
rows: tuple[ReuseRow, ...]
|
|
454
|
+
project_metadata_available: bool = True
|
|
455
|
+
sort: str = "date"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def build_codex_reuse_result(
|
|
459
|
+
entries: Iterable[QualifiedCodexEntry], *, group_by: Literal["date", "session", "model"] = "date",
|
|
460
|
+
project_metadata_available: bool = True, sort: str | None = None,
|
|
461
|
+
range_start: dt.datetime | None = None, range_end: dt.datetime | None = None,
|
|
462
|
+
) -> SourceResult[CodexReuseData]:
|
|
463
|
+
"""Build token-reuse rows with Codex-inclusive token semantics."""
|
|
464
|
+
if (range_start is None) != (range_end is None):
|
|
465
|
+
raise ValueError("Codex reuse range_start and range_end must be supplied together")
|
|
466
|
+
if range_start is not None and range_end is not None:
|
|
467
|
+
requested_start = _require_aware(range_start, "range_start")
|
|
468
|
+
requested_end = _require_aware(range_end, "range_end")
|
|
469
|
+
if requested_end <= requested_start:
|
|
470
|
+
raise ValueError("Codex reuse range_end must be after range_start")
|
|
471
|
+
values = tuple(
|
|
472
|
+
entry for entry in entries
|
|
473
|
+
if _entry_in_half_open(entry, requested_start, requested_end)
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
requested_start = requested_end = None
|
|
477
|
+
values = tuple(entries)
|
|
478
|
+
if group_by not in {"date", "session", "model"}:
|
|
479
|
+
raise ValueError("group_by must be date, session, or model")
|
|
480
|
+
grouped: dict[tuple[str, ...], list[QualifiedCodexEntry]] = defaultdict(list)
|
|
481
|
+
for entry in values:
|
|
482
|
+
timestamp = _require_aware(entry.timestamp, "entry timestamp")
|
|
483
|
+
if group_by == "date":
|
|
484
|
+
key = (timestamp.date().isoformat(),)
|
|
485
|
+
elif group_by == "session":
|
|
486
|
+
key = (entry.source_root_key, entry.conversation_key)
|
|
487
|
+
else:
|
|
488
|
+
key = (entry.model,)
|
|
489
|
+
grouped[key].append(entry)
|
|
490
|
+
|
|
491
|
+
rows: list[ReuseRow] = []
|
|
492
|
+
for key, group in grouped.items():
|
|
493
|
+
ordered = tuple(sorted(group, key=lambda entry: entry.timestamp))
|
|
494
|
+
by_model: dict[str, list[QualifiedCodexEntry]] = defaultdict(list)
|
|
495
|
+
for entry in ordered:
|
|
496
|
+
by_model[entry.model].append(entry)
|
|
497
|
+
if group_by == "date":
|
|
498
|
+
row_key, label = _opaque_key("reuse-date", key[0]), key[0]
|
|
499
|
+
project_key = project_label = None
|
|
500
|
+
elif group_by == "session":
|
|
501
|
+
row_key = _opaque_key("reuse-session", key[0], key[1])
|
|
502
|
+
label = "Codex session"
|
|
503
|
+
project_key = ordered[0].project_key if project_metadata_available else None
|
|
504
|
+
project_label = emitted_project_label(ordered[0]) if project_metadata_available else None
|
|
505
|
+
else:
|
|
506
|
+
row_key, label = _opaque_key("reuse-model", key[0]), key[0]
|
|
507
|
+
project_key = project_label = None
|
|
508
|
+
rows.append(ReuseRow(
|
|
509
|
+
key=row_key,
|
|
510
|
+
label=label,
|
|
511
|
+
project_key=project_key,
|
|
512
|
+
project_label=project_label,
|
|
513
|
+
last_activity=_require_aware(ordered[-1].timestamp, "entry timestamp"),
|
|
514
|
+
totals=_totals(ordered),
|
|
515
|
+
models=tuple((model, _totals(by_model[model])) for model in sorted(by_model)),
|
|
516
|
+
))
|
|
517
|
+
resolved_sort = sort or ("recent" if group_by == "session" else "date")
|
|
518
|
+
if resolved_sort not in {"date", "recent", "cost", "reuse"}:
|
|
519
|
+
raise ValueError("Codex reuse sort must be date, recent, cost, or reuse")
|
|
520
|
+
if resolved_sort == "recent":
|
|
521
|
+
rows.sort(key=lambda row: (row.last_activity, row.label, row.key), reverse=True)
|
|
522
|
+
elif resolved_sort == "cost":
|
|
523
|
+
rows.sort(key=lambda row: (row.totals.cost_usd, row.label, row.key), reverse=True)
|
|
524
|
+
elif resolved_sort == "reuse":
|
|
525
|
+
rows.sort(
|
|
526
|
+
key=lambda row: (
|
|
527
|
+
row.totals.cached_input_percent if row.totals.cached_input_percent is not None else -1.0,
|
|
528
|
+
row.label, row.key,
|
|
529
|
+
),
|
|
530
|
+
reverse=True,
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
rows.sort(key=lambda row: (row.label, row.key))
|
|
534
|
+
timestamps = sorted(_require_aware(entry.timestamp, "entry timestamp") for entry in values)
|
|
535
|
+
data = CodexReuseData(
|
|
536
|
+
requested_start if requested_start is not None else (timestamps[0] if timestamps else None),
|
|
537
|
+
requested_end if requested_end is not None else (timestamps[-1] if timestamps else None),
|
|
538
|
+
group_by,
|
|
539
|
+
_totals(values),
|
|
540
|
+
tuple(rows),
|
|
541
|
+
project_metadata_available,
|
|
542
|
+
resolved_sort,
|
|
543
|
+
)
|
|
544
|
+
status: Availability = "empty" if not values else "ok"
|
|
545
|
+
warnings: tuple[SourceWarning, ...] = ()
|
|
546
|
+
if not project_metadata_available:
|
|
547
|
+
status = "partial"
|
|
548
|
+
warnings = (QUALIFIED_METADATA_WARNING,)
|
|
549
|
+
return SourceResult("codex", status, data, warnings)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@dataclass(frozen=True)
|
|
553
|
+
class DiffWindowTotals:
|
|
554
|
+
window: AnalyticsWindow
|
|
555
|
+
totals: TokenTotals
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@dataclass(frozen=True)
|
|
559
|
+
class DiffRow:
|
|
560
|
+
key: str
|
|
561
|
+
label: str
|
|
562
|
+
status: Literal["changed", "new", "dropped"]
|
|
563
|
+
a: TokenTotals
|
|
564
|
+
b: TokenTotals
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@dataclass(frozen=True)
|
|
568
|
+
class DiffSection:
|
|
569
|
+
key: str
|
|
570
|
+
label: str
|
|
571
|
+
status: Literal["ok", "empty", "unavailable"]
|
|
572
|
+
rows: tuple[DiffRow, ...]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@dataclass(frozen=True)
|
|
576
|
+
class CodexDiffData:
|
|
577
|
+
windows: tuple[DiffWindowTotals, DiffWindowTotals]
|
|
578
|
+
combined_a: TokenTotals
|
|
579
|
+
combined_b: TokenTotals
|
|
580
|
+
sections: tuple[DiffSection, ...]
|
|
581
|
+
only: str | None = None
|
|
582
|
+
smart_filter: bool = False
|
|
583
|
+
sort: str = "cost"
|
|
584
|
+
limit: int | None = None
|
|
585
|
+
normalization: Literal["none", "per-day"] | None = None
|
|
586
|
+
min_delta_usd: float | None = None
|
|
587
|
+
min_delta_pct: float | None = None
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@dataclass(frozen=True)
|
|
591
|
+
class CodexReportRow:
|
|
592
|
+
block_start: dt.datetime
|
|
593
|
+
reset_at: dt.datetime
|
|
594
|
+
used_percent: float | None
|
|
595
|
+
cost_usd: float
|
|
596
|
+
cost_per_percent: float | None
|
|
597
|
+
status: Literal["ok", "unavailable"]
|
|
598
|
+
detail: tuple["CodexReportDetailRow", ...] = ()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@dataclass(frozen=True)
|
|
602
|
+
class CodexReportDetailRow:
|
|
603
|
+
"""One native quota-percent crossing within a selected reset block."""
|
|
604
|
+
|
|
605
|
+
percent_threshold: int
|
|
606
|
+
captured_at: dt.datetime
|
|
607
|
+
cumulative_cost_usd: float
|
|
608
|
+
marginal_cost_usd: float
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@dataclass(frozen=True)
|
|
612
|
+
class CodexReportSeries:
|
|
613
|
+
quota_key: str
|
|
614
|
+
slot: str
|
|
615
|
+
window_minutes: int
|
|
616
|
+
rows: tuple[CodexReportRow, ...]
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
@dataclass(frozen=True)
|
|
620
|
+
class CodexReportData:
|
|
621
|
+
as_of: dt.datetime
|
|
622
|
+
series: tuple[CodexReportSeries, ...]
|
|
623
|
+
quota_status: Literal["ok", "empty", "unavailable"]
|
|
624
|
+
quota_warnings: tuple[SourceWarning, ...] = ()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def build_codex_report_result(
|
|
628
|
+
entries: Iterable[QualifiedCodexEntry], blocks: Iterable[object], *,
|
|
629
|
+
as_of: dt.datetime, quota_available: bool = True, include_detail: bool = False,
|
|
630
|
+
) -> SourceResult[CodexReportData | None]:
|
|
631
|
+
"""Build per-logical-limit quota accounting without cross-limit arithmetic.
|
|
632
|
+
|
|
633
|
+
A physical Codex accounting entry can truthfully participate in more than
|
|
634
|
+
one logical-limit series. This function therefore never sums series, has
|
|
635
|
+
no combined field, and deliberately does not receive project metadata.
|
|
636
|
+
"""
|
|
637
|
+
as_of_utc = _require_aware(as_of, "as_of")
|
|
638
|
+
if not quota_available:
|
|
639
|
+
data = CodexReportData(
|
|
640
|
+
as_of_utc, (), "unavailable", (QUOTA_STATE_WARNING,),
|
|
641
|
+
)
|
|
642
|
+
return SourceResult("codex", "partial", data, (QUOTA_STATE_WARNING,))
|
|
643
|
+
values = tuple(entries)
|
|
644
|
+
grouped: dict[tuple[str, str, str, str, int], list[CodexReportRow]] = defaultdict(list)
|
|
645
|
+
for block in blocks:
|
|
646
|
+
identity = block.identity
|
|
647
|
+
if getattr(identity, "source", None) != "codex":
|
|
648
|
+
continue
|
|
649
|
+
nominal_start = _require_aware(block.nominal_start_at, "block nominal_start_at")
|
|
650
|
+
reset_at = _require_aware(block.resets_at, "block resets_at")
|
|
651
|
+
block_end = min(as_of_utc, reset_at)
|
|
652
|
+
if block_end <= nominal_start:
|
|
653
|
+
continue
|
|
654
|
+
block_entries = tuple(
|
|
655
|
+
entry for entry in values
|
|
656
|
+
if entry.source_root_key == identity.source_root_key
|
|
657
|
+
and _entry_in_half_open(entry, nominal_start, block_end)
|
|
658
|
+
)
|
|
659
|
+
totals = _totals(block_entries)
|
|
660
|
+
used_percent = float(block.current_percent)
|
|
661
|
+
key = (
|
|
662
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
663
|
+
identity.observed_slot, identity.window_minutes,
|
|
664
|
+
)
|
|
665
|
+
detail: tuple[CodexReportDetailRow, ...] = ()
|
|
666
|
+
if include_detail:
|
|
667
|
+
observations = tuple(sorted(
|
|
668
|
+
getattr(block, "observations", ()),
|
|
669
|
+
key=lambda observation: observation.captured_at,
|
|
670
|
+
))
|
|
671
|
+
if observations:
|
|
672
|
+
prior_percent = math.floor(float(observations[0].used_percent) + 1e-9)
|
|
673
|
+
prior_boundary = observations[0].captured_at
|
|
674
|
+
cumulative_cost = 0.0
|
|
675
|
+
detail_rows: list[CodexReportDetailRow] = []
|
|
676
|
+
for observation in observations[1:]:
|
|
677
|
+
current_percent = math.floor(float(observation.used_percent) + 1e-9)
|
|
678
|
+
if current_percent <= prior_percent:
|
|
679
|
+
continue
|
|
680
|
+
marginal_cost = math.fsum(
|
|
681
|
+
entry.cost_usd for entry in block_entries
|
|
682
|
+
if prior_boundary < entry.timestamp <= observation.captured_at
|
|
683
|
+
)
|
|
684
|
+
cumulative_cost += marginal_cost
|
|
685
|
+
for threshold in range(prior_percent + 1, current_percent + 1):
|
|
686
|
+
detail_rows.append(CodexReportDetailRow(
|
|
687
|
+
percent_threshold=threshold,
|
|
688
|
+
captured_at=observation.captured_at,
|
|
689
|
+
cumulative_cost_usd=cumulative_cost,
|
|
690
|
+
marginal_cost_usd=(
|
|
691
|
+
marginal_cost if threshold == prior_percent + 1 else 0.0
|
|
692
|
+
),
|
|
693
|
+
))
|
|
694
|
+
prior_percent = current_percent
|
|
695
|
+
prior_boundary = observation.captured_at
|
|
696
|
+
detail = tuple(detail_rows)
|
|
697
|
+
grouped[key].append(CodexReportRow(
|
|
698
|
+
block_start=nominal_start,
|
|
699
|
+
reset_at=reset_at,
|
|
700
|
+
used_percent=used_percent,
|
|
701
|
+
cost_usd=totals.cost_usd,
|
|
702
|
+
cost_per_percent=(totals.cost_usd / used_percent if used_percent > 0 else None),
|
|
703
|
+
status="ok",
|
|
704
|
+
detail=detail,
|
|
705
|
+
))
|
|
706
|
+
series: list[CodexReportSeries] = []
|
|
707
|
+
for (source, root_key, limit_key, slot, window_minutes), rows in sorted(grouped.items()):
|
|
708
|
+
series.append(CodexReportSeries(
|
|
709
|
+
quota_key=opaque_quota_key(source, root_key, limit_key, slot, window_minutes),
|
|
710
|
+
slot=slot,
|
|
711
|
+
window_minutes=window_minutes,
|
|
712
|
+
rows=tuple(sorted(rows, key=lambda row: (row.reset_at, row.block_start))),
|
|
713
|
+
))
|
|
714
|
+
status: Literal["ok", "empty", "unavailable"] = "empty" if not series else "ok"
|
|
715
|
+
data = CodexReportData(as_of_utc, tuple(series), status)
|
|
716
|
+
return SourceResult("codex", status, data)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _diff_rows(
|
|
720
|
+
a_entries: Iterable[QualifiedCodexEntry], b_entries: Iterable[QualifiedCodexEntry],
|
|
721
|
+
*, key_fn, label_fn, classify_presence: bool,
|
|
722
|
+
) -> tuple[DiffRow, ...]:
|
|
723
|
+
a_groups: dict[str, list[QualifiedCodexEntry]] = defaultdict(list)
|
|
724
|
+
b_groups: dict[str, list[QualifiedCodexEntry]] = defaultdict(list)
|
|
725
|
+
labels: dict[str, str] = {}
|
|
726
|
+
for entry in a_entries:
|
|
727
|
+
key = key_fn(entry)
|
|
728
|
+
a_groups[key].append(entry)
|
|
729
|
+
labels.setdefault(key, label_fn(entry))
|
|
730
|
+
for entry in b_entries:
|
|
731
|
+
key = key_fn(entry)
|
|
732
|
+
b_groups[key].append(entry)
|
|
733
|
+
labels.setdefault(key, label_fn(entry))
|
|
734
|
+
rows: list[DiffRow] = []
|
|
735
|
+
for key in sorted(set(a_groups) | set(b_groups)):
|
|
736
|
+
has_a, has_b = key in a_groups, key in b_groups
|
|
737
|
+
a = _totals(a_groups[key])
|
|
738
|
+
b = _totals(b_groups[key])
|
|
739
|
+
status: Literal["changed", "new", "dropped"] = "changed"
|
|
740
|
+
if classify_presence:
|
|
741
|
+
if not has_a:
|
|
742
|
+
status = "new"
|
|
743
|
+
elif not has_b:
|
|
744
|
+
status = "dropped"
|
|
745
|
+
rows.append(DiffRow(key, labels[key], status, a, b))
|
|
746
|
+
return tuple(rows)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _diff_window_length_days(window: AnalyticsWindow) -> float:
|
|
750
|
+
seconds = (window.end_at - window.start_at).total_seconds()
|
|
751
|
+
if seconds <= 0:
|
|
752
|
+
raise ValueError("Codex diff windows must have a positive length")
|
|
753
|
+
return seconds / 86_400.0
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def resolve_codex_diff_normalization(
|
|
757
|
+
window_a: AnalyticsWindow, window_b: AnalyticsWindow, *, allow_mismatch: bool,
|
|
758
|
+
) -> Literal["none", "per-day"]:
|
|
759
|
+
"""Validate two source windows before provider reads and select their units."""
|
|
760
|
+
a_days = _diff_window_length_days(window_a)
|
|
761
|
+
b_days = _diff_window_length_days(window_b)
|
|
762
|
+
mismatched = abs(a_days - b_days) > 0.01
|
|
763
|
+
if not mismatched:
|
|
764
|
+
return "none"
|
|
765
|
+
same_eligible_kind = (
|
|
766
|
+
window_a.kind == window_b.kind and window_a.kind in {"week", "month"}
|
|
767
|
+
)
|
|
768
|
+
if not same_eligible_kind and not allow_mismatch:
|
|
769
|
+
raise ValueError(
|
|
770
|
+
f"window A is {a_days:.1f} days, window B is {b_days:.1f} days; "
|
|
771
|
+
"pass --allow-mismatch to compare anyway with per-day normalization"
|
|
772
|
+
)
|
|
773
|
+
return "per-day"
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _normalized_diff_totals(totals: TokenTotals, *, days: float, per_day: bool) -> TokenTotals:
|
|
777
|
+
if not per_day:
|
|
778
|
+
return totals
|
|
779
|
+
return TokenTotals(
|
|
780
|
+
input_tokens=totals.input_tokens,
|
|
781
|
+
cached_input_tokens=totals.cached_input_tokens,
|
|
782
|
+
output_tokens=totals.output_tokens,
|
|
783
|
+
reasoning_output_tokens=totals.reasoning_output_tokens,
|
|
784
|
+
total_tokens=totals.total_tokens,
|
|
785
|
+
cost_usd=totals.cost_usd / days,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _normalized_diff_rows(
|
|
790
|
+
rows: Iterable[DiffRow], *, a_days: float, b_days: float, per_day: bool,
|
|
791
|
+
) -> tuple[DiffRow, ...]:
|
|
792
|
+
return tuple(
|
|
793
|
+
DiffRow(
|
|
794
|
+
row.key, row.label, row.status,
|
|
795
|
+
_normalized_diff_totals(row.a, days=a_days, per_day=per_day),
|
|
796
|
+
_normalized_diff_totals(row.b, days=b_days, per_day=per_day),
|
|
797
|
+
)
|
|
798
|
+
for row in rows
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _diff_row_cost_pct(row: DiffRow) -> float:
|
|
803
|
+
delta = row.b.cost_usd - row.a.cost_usd
|
|
804
|
+
if row.a.cost_usd == 0:
|
|
805
|
+
return 0.0 if delta == 0 else math.inf
|
|
806
|
+
return delta / row.a.cost_usd * 100.0
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _sort_diff_rows(rows: Iterable[DiffRow], sort: str) -> tuple[DiffRow, ...]:
|
|
810
|
+
if sort == "delta":
|
|
811
|
+
key = lambda row: (-abs(row.b.cost_usd - row.a.cost_usd), row.label, row.key)
|
|
812
|
+
elif sort in {"cost", "cost-a"}:
|
|
813
|
+
key = lambda row: (-row.a.cost_usd, row.label, row.key)
|
|
814
|
+
elif sort == "cost-b":
|
|
815
|
+
key = lambda row: (-row.b.cost_usd, row.label, row.key)
|
|
816
|
+
elif sort == "name":
|
|
817
|
+
key = lambda row: (row.label, row.key)
|
|
818
|
+
elif sort == "status":
|
|
819
|
+
order = {"dropped": 0, "changed": 1, "new": 2}
|
|
820
|
+
key = lambda row: (
|
|
821
|
+
order[row.status], -abs(row.b.cost_usd - row.a.cost_usd), row.label, row.key,
|
|
822
|
+
)
|
|
823
|
+
else:
|
|
824
|
+
raise ValueError("Codex diff sort must be delta, cost-a, cost-b, name, or status")
|
|
825
|
+
return tuple(sorted(rows, key=key))
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _visible_diff_rows(
|
|
829
|
+
rows: Iterable[DiffRow], *, smart_filter: bool, min_delta_usd: float,
|
|
830
|
+
min_delta_pct: float, sort: str, top: int | None,
|
|
831
|
+
) -> tuple[DiffRow, ...]:
|
|
832
|
+
"""Mirror the legacy diff filter/sort/top semantics for one provider leg."""
|
|
833
|
+
visible: list[DiffRow] = []
|
|
834
|
+
for row in rows:
|
|
835
|
+
if (
|
|
836
|
+
smart_filter
|
|
837
|
+
and row.status == "changed"
|
|
838
|
+
and abs(row.b.cost_usd - row.a.cost_usd) < min_delta_usd
|
|
839
|
+
and abs(_diff_row_cost_pct(row)) < min_delta_pct
|
|
840
|
+
):
|
|
841
|
+
continue
|
|
842
|
+
visible.append(row)
|
|
843
|
+
visible = list(_sort_diff_rows(visible, sort))
|
|
844
|
+
if top is not None and top >= 0:
|
|
845
|
+
changed = [row for row in visible if row.status == "changed"]
|
|
846
|
+
terminal = [row for row in visible if row.status != "changed"]
|
|
847
|
+
visible = list(_sort_diff_rows((*terminal, *changed[:top]), sort))
|
|
848
|
+
return tuple(visible)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def build_codex_diff_result(
|
|
852
|
+
entries: Iterable[QualifiedCodexEntry], window_a: AnalyticsWindow,
|
|
853
|
+
window_b: AnalyticsWindow, *, project_metadata_available: bool = True,
|
|
854
|
+
only: str | None = None, allow_mismatch: bool = False, show_all: bool = True,
|
|
855
|
+
min_delta_usd: float = 0.10, min_delta_pct: float = 1.0,
|
|
856
|
+
sort: str = "cost", top: int | None = None,
|
|
857
|
+
normalization: Literal["none", "per-day"] | None = None,
|
|
858
|
+
classify_presence: bool = False,
|
|
859
|
+
) -> SourceResult[CodexDiffData]:
|
|
860
|
+
"""Compare two half-open windows without a Claude cache section."""
|
|
861
|
+
# The command validates exposed windows before provider I/O. Keep this
|
|
862
|
+
# pure builder permissive for direct in-process callers that use it to
|
|
863
|
+
# inspect arbitrary half-open windows; explicit normalisation is the
|
|
864
|
+
# command-to-kernel contract.
|
|
865
|
+
if normalization is None:
|
|
866
|
+
normalization = (
|
|
867
|
+
resolve_codex_diff_normalization(
|
|
868
|
+
window_a, window_b, allow_mismatch=True,
|
|
869
|
+
) if allow_mismatch else "none"
|
|
870
|
+
)
|
|
871
|
+
per_day = normalization == "per-day"
|
|
872
|
+
a_days, b_days = _diff_window_length_days(window_a), _diff_window_length_days(window_b)
|
|
873
|
+
values = tuple(entries)
|
|
874
|
+
a_entries = tuple(entry for entry in values if _entry_in_half_open(entry, window_a.start_at, window_a.end_at))
|
|
875
|
+
b_entries = tuple(entry for entry in values if _entry_in_half_open(entry, window_b.start_at, window_b.end_at))
|
|
876
|
+
overall_rows = _diff_rows(
|
|
877
|
+
a_entries, b_entries, key_fn=lambda _entry: "overall", label_fn=lambda _entry: "Overall",
|
|
878
|
+
classify_presence=classify_presence,
|
|
879
|
+
)
|
|
880
|
+
model_rows = _diff_rows(
|
|
881
|
+
a_entries, b_entries, key_fn=lambda entry: entry.model, label_fn=lambda entry: entry.model,
|
|
882
|
+
classify_presence=classify_presence,
|
|
883
|
+
)
|
|
884
|
+
project_rows = _diff_rows(
|
|
885
|
+
a_entries, b_entries, key_fn=lambda entry: entry.project_key,
|
|
886
|
+
label_fn=emitted_project_label,
|
|
887
|
+
classify_presence=classify_presence,
|
|
888
|
+
)
|
|
889
|
+
reuse_rows = _diff_rows(
|
|
890
|
+
a_entries, b_entries, key_fn=lambda entry: entry.model, label_fn=lambda entry: entry.model,
|
|
891
|
+
classify_presence=classify_presence,
|
|
892
|
+
)
|
|
893
|
+
overall_rows = _normalized_diff_rows(
|
|
894
|
+
overall_rows, a_days=a_days, b_days=b_days, per_day=per_day,
|
|
895
|
+
)
|
|
896
|
+
model_rows = _visible_diff_rows(
|
|
897
|
+
_normalized_diff_rows(model_rows, a_days=a_days, b_days=b_days, per_day=per_day),
|
|
898
|
+
smart_filter=not show_all, min_delta_usd=min_delta_usd,
|
|
899
|
+
min_delta_pct=min_delta_pct, sort=sort, top=top,
|
|
900
|
+
)
|
|
901
|
+
project_rows = _visible_diff_rows(
|
|
902
|
+
_normalized_diff_rows(project_rows, a_days=a_days, b_days=b_days, per_day=per_day),
|
|
903
|
+
smart_filter=not show_all, min_delta_usd=min_delta_usd,
|
|
904
|
+
min_delta_pct=min_delta_pct, sort=sort, top=top,
|
|
905
|
+
)
|
|
906
|
+
reuse_rows = _visible_diff_rows(
|
|
907
|
+
_normalized_diff_rows(reuse_rows, a_days=a_days, b_days=b_days, per_day=per_day),
|
|
908
|
+
smart_filter=not show_all, min_delta_usd=min_delta_usd,
|
|
909
|
+
min_delta_pct=min_delta_pct, sort=sort, top=top,
|
|
910
|
+
)
|
|
911
|
+
sections = (
|
|
912
|
+
DiffSection("overall", "Overall", "empty" if not overall_rows else "ok", overall_rows),
|
|
913
|
+
DiffSection("models", "Models", "empty" if not model_rows else "ok", model_rows),
|
|
914
|
+
DiffSection(
|
|
915
|
+
"projects", "Projects",
|
|
916
|
+
"unavailable" if not project_metadata_available else ("empty" if not project_rows else "ok"),
|
|
917
|
+
() if not project_metadata_available else project_rows,
|
|
918
|
+
),
|
|
919
|
+
DiffSection("token-reuse", "Token reuse", "empty" if not reuse_rows else "ok", reuse_rows),
|
|
920
|
+
)
|
|
921
|
+
selected = None if only is None else {
|
|
922
|
+
item.strip() for item in only.split(",") if item.strip()
|
|
923
|
+
}
|
|
924
|
+
if selected is not None:
|
|
925
|
+
supported = {section.key for section in sections}
|
|
926
|
+
unknown = selected - supported
|
|
927
|
+
if unknown:
|
|
928
|
+
raise ValueError(
|
|
929
|
+
"Codex diff --only contains unknown section(s): "
|
|
930
|
+
+ ", ".join(sorted(unknown))
|
|
931
|
+
)
|
|
932
|
+
sections = tuple(section for section in sections if section.key in selected)
|
|
933
|
+
data = CodexDiffData(
|
|
934
|
+
(
|
|
935
|
+
DiffWindowTotals(
|
|
936
|
+
window_a, _normalized_diff_totals(_totals(a_entries), days=a_days, per_day=per_day),
|
|
937
|
+
),
|
|
938
|
+
DiffWindowTotals(
|
|
939
|
+
window_b, _normalized_diff_totals(_totals(b_entries), days=b_days, per_day=per_day),
|
|
940
|
+
),
|
|
941
|
+
),
|
|
942
|
+
_normalized_diff_totals(_totals(a_entries), days=a_days, per_day=per_day),
|
|
943
|
+
_normalized_diff_totals(_totals(b_entries), days=b_days, per_day=per_day), sections,
|
|
944
|
+
only=only,
|
|
945
|
+
smart_filter=not show_all,
|
|
946
|
+
sort=sort,
|
|
947
|
+
limit=top,
|
|
948
|
+
normalization=(normalization if normalization != "none" else None),
|
|
949
|
+
min_delta_usd=(min_delta_usd if classify_presence else None),
|
|
950
|
+
min_delta_pct=(min_delta_pct if classify_presence else None),
|
|
951
|
+
)
|
|
952
|
+
status: Availability = "empty" if not a_entries and not b_entries else "ok"
|
|
953
|
+
warnings: tuple[SourceWarning, ...] = ()
|
|
954
|
+
if any(section.status == "unavailable" for section in sections):
|
|
955
|
+
status = "partial"
|
|
956
|
+
warnings = (QUALIFIED_METADATA_WARNING,)
|
|
957
|
+
return SourceResult("codex", status, data, warnings)
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _iso_z(value: dt.datetime) -> str:
|
|
961
|
+
return _require_aware(value, "timestamp").isoformat().replace("+00:00", "Z")
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _project_wire(row: CodexProjectRow, *, include_breakdown: bool) -> dict[str, object]:
|
|
965
|
+
result: dict[str, object] = {
|
|
966
|
+
"projectKey": row.project_key,
|
|
967
|
+
"displayLabel": row.display_label,
|
|
968
|
+
"sessionCount": row.session_count,
|
|
969
|
+
"firstSeen": _iso_z(row.first_seen),
|
|
970
|
+
"lastSeen": _iso_z(row.last_seen),
|
|
971
|
+
"tokens": _codex_tokens_wire(row.totals),
|
|
972
|
+
"costUsd": row.totals.cost_usd,
|
|
973
|
+
"quotaAttributions": [
|
|
974
|
+
{
|
|
975
|
+
"quotaKey": attribution.quota_key,
|
|
976
|
+
"slot": attribution.slot,
|
|
977
|
+
"windowMinutes": attribution.window_minutes,
|
|
978
|
+
"resetAt": _iso_z(attribution.reset_at) if attribution.reset_at else None,
|
|
979
|
+
"usedPercent": attribution.used_percent,
|
|
980
|
+
"attributedUsedPercent": attribution.attributed_used_percent,
|
|
981
|
+
"costPerPercent": attribution.cost_per_percent,
|
|
982
|
+
"status": attribution.status,
|
|
983
|
+
}
|
|
984
|
+
for attribution in row.quota_attributions
|
|
985
|
+
],
|
|
986
|
+
}
|
|
987
|
+
if include_breakdown:
|
|
988
|
+
result["models"] = [
|
|
989
|
+
{"model": model, "costUsd": totals.cost_usd,
|
|
990
|
+
**_codex_tokens_wire(totals)}
|
|
991
|
+
for model, totals in row.models
|
|
992
|
+
]
|
|
993
|
+
return result
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _range_wire(data: CodexRangeData) -> dict[str, object]:
|
|
997
|
+
result: dict[str, object] = {
|
|
998
|
+
"start": _iso_z(data.start),
|
|
999
|
+
"end": _iso_z(data.end),
|
|
1000
|
+
"totals": {"costUsd": data.totals.cost_usd, **_codex_tokens_wire(data.totals)},
|
|
1001
|
+
}
|
|
1002
|
+
result["models"] = (
|
|
1003
|
+
[
|
|
1004
|
+
{"model": model, "costUsd": totals.cost_usd, **_codex_tokens_wire(totals)}
|
|
1005
|
+
for model, totals in data.models
|
|
1006
|
+
]
|
|
1007
|
+
if data.include_breakdown else []
|
|
1008
|
+
)
|
|
1009
|
+
return result
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _reuse_row_wire(row: ReuseRow) -> dict[str, object]:
|
|
1013
|
+
return {
|
|
1014
|
+
"key": row.key,
|
|
1015
|
+
"label": row.label,
|
|
1016
|
+
"projectKey": row.project_key,
|
|
1017
|
+
"projectLabel": row.project_label,
|
|
1018
|
+
"lastActivity": _iso_z(row.last_activity),
|
|
1019
|
+
"costUsd": row.totals.cost_usd,
|
|
1020
|
+
**_codex_tokens_wire(row.totals, cached_percent=True),
|
|
1021
|
+
"models": [
|
|
1022
|
+
{"model": model, "costUsd": totals.cost_usd,
|
|
1023
|
+
**_codex_tokens_wire(totals, cached_percent=True)}
|
|
1024
|
+
for model, totals in row.models
|
|
1025
|
+
],
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _reuse_wire(data: CodexReuseData) -> dict[str, object]:
|
|
1030
|
+
return {
|
|
1031
|
+
"start": _iso_z(data.start) if data.start else None,
|
|
1032
|
+
"end": _iso_z(data.end) if data.end else None,
|
|
1033
|
+
"groupBy": data.group_by,
|
|
1034
|
+
"totals": {
|
|
1035
|
+
"costUsd": data.totals.cost_usd,
|
|
1036
|
+
**_codex_tokens_wire(data.totals, cached_percent=True),
|
|
1037
|
+
},
|
|
1038
|
+
"sections": [
|
|
1039
|
+
{
|
|
1040
|
+
"key": "token-reuse",
|
|
1041
|
+
"status": "empty" if not data.rows else "ok",
|
|
1042
|
+
"data": {"rows": [_reuse_row_wire(row) for row in data.rows]},
|
|
1043
|
+
"warnings": [],
|
|
1044
|
+
},
|
|
1045
|
+
unavailable_section("project-metadata", QUALIFIED_METADATA_WARNING)
|
|
1046
|
+
if not data.project_metadata_available else {
|
|
1047
|
+
"key": "project-metadata",
|
|
1048
|
+
"status": "empty" if not data.rows else "ok",
|
|
1049
|
+
"data": {"projects": []},
|
|
1050
|
+
"warnings": [],
|
|
1051
|
+
},
|
|
1052
|
+
],
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _diff_delta(a: TokenTotals, b: TokenTotals) -> dict[str, object]:
|
|
1057
|
+
return {
|
|
1058
|
+
"cost_usd": b.cost_usd - a.cost_usd,
|
|
1059
|
+
"total_tokens": b.total_tokens - a.total_tokens,
|
|
1060
|
+
"input_tokens": b.input_tokens - a.input_tokens,
|
|
1061
|
+
"cached_input_tokens": b.cached_input_tokens - a.cached_input_tokens,
|
|
1062
|
+
"output_tokens": b.output_tokens - a.output_tokens,
|
|
1063
|
+
"reasoning_output_tokens": b.reasoning_output_tokens - a.reasoning_output_tokens,
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _diff_wire(data: CodexDiffData) -> dict[str, object]:
|
|
1068
|
+
window_a, window_b = data.windows
|
|
1069
|
+
options: dict[str, object] = {
|
|
1070
|
+
"only": data.only,
|
|
1071
|
+
"smart_filter": data.smart_filter,
|
|
1072
|
+
"sort": data.sort,
|
|
1073
|
+
"limit": data.limit,
|
|
1074
|
+
}
|
|
1075
|
+
if data.normalization is not None:
|
|
1076
|
+
options["normalization"] = data.normalization
|
|
1077
|
+
if data.min_delta_usd is not None:
|
|
1078
|
+
options["min_delta_usd"] = data.min_delta_usd
|
|
1079
|
+
if data.min_delta_pct is not None:
|
|
1080
|
+
options["min_delta_pct"] = data.min_delta_pct
|
|
1081
|
+
return {
|
|
1082
|
+
"windows": {
|
|
1083
|
+
"a": {
|
|
1084
|
+
"label": window_a.window.label,
|
|
1085
|
+
"kind": window_a.window.kind,
|
|
1086
|
+
"start_at": _iso_z(window_a.window.start_at),
|
|
1087
|
+
"end_at": _iso_z(window_a.window.end_at),
|
|
1088
|
+
"used_pct_mode": window_a.window.used_pct_mode,
|
|
1089
|
+
},
|
|
1090
|
+
"b": {
|
|
1091
|
+
"label": window_b.window.label,
|
|
1092
|
+
"kind": window_b.window.kind,
|
|
1093
|
+
"start_at": _iso_z(window_b.window.start_at),
|
|
1094
|
+
"end_at": _iso_z(window_b.window.end_at),
|
|
1095
|
+
"used_pct_mode": window_b.window.used_pct_mode,
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
"combined": {
|
|
1099
|
+
"cost_usd": {
|
|
1100
|
+
"a": data.combined_a.cost_usd,
|
|
1101
|
+
"b": data.combined_b.cost_usd,
|
|
1102
|
+
"delta": data.combined_b.cost_usd - data.combined_a.cost_usd,
|
|
1103
|
+
},
|
|
1104
|
+
"total_tokens": {
|
|
1105
|
+
"a": data.combined_a.total_tokens,
|
|
1106
|
+
"b": data.combined_b.total_tokens,
|
|
1107
|
+
"delta": data.combined_b.total_tokens - data.combined_a.total_tokens,
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
"sections": [
|
|
1111
|
+
unavailable_section(section.key, QUALIFIED_METADATA_WARNING)
|
|
1112
|
+
if section.status == "unavailable" else {
|
|
1113
|
+
"key": section.key,
|
|
1114
|
+
"label": section.label,
|
|
1115
|
+
"status": section.status,
|
|
1116
|
+
"data": {"rows": [
|
|
1117
|
+
{
|
|
1118
|
+
"key": row.key,
|
|
1119
|
+
"label": row.label,
|
|
1120
|
+
"status": row.status,
|
|
1121
|
+
"a": _metric_wire(row.a),
|
|
1122
|
+
"b": _metric_wire(row.b),
|
|
1123
|
+
"delta": _diff_delta(row.a, row.b),
|
|
1124
|
+
}
|
|
1125
|
+
for row in section.rows
|
|
1126
|
+
]},
|
|
1127
|
+
"warnings": [],
|
|
1128
|
+
}
|
|
1129
|
+
for section in data.sections
|
|
1130
|
+
],
|
|
1131
|
+
"options": options,
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def unavailable_section(key: str, warning: SourceWarning) -> dict[str, object]:
|
|
1136
|
+
"""Return the frozen unavailable sibling-section shape."""
|
|
1137
|
+
return {
|
|
1138
|
+
"key": key,
|
|
1139
|
+
"status": "unavailable",
|
|
1140
|
+
"data": None,
|
|
1141
|
+
"warnings": [{"code": warning.code, "message": warning.message}],
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _report_wire(data: CodexReportData) -> dict[str, object]:
|
|
1146
|
+
section_data: dict[str, object] | None = {
|
|
1147
|
+
"series": [
|
|
1148
|
+
{
|
|
1149
|
+
"quotaKey": series.quota_key,
|
|
1150
|
+
"sourceLabel": "Codex",
|
|
1151
|
+
"slot": series.slot,
|
|
1152
|
+
"windowMinutes": series.window_minutes,
|
|
1153
|
+
"rows": [
|
|
1154
|
+
{
|
|
1155
|
+
"blockStart": _iso_z(row.block_start),
|
|
1156
|
+
"resetAt": _iso_z(row.reset_at),
|
|
1157
|
+
"usedPercent": row.used_percent,
|
|
1158
|
+
"costUsd": row.cost_usd,
|
|
1159
|
+
"costPerPercent": row.cost_per_percent,
|
|
1160
|
+
"status": row.status,
|
|
1161
|
+
"detail": [
|
|
1162
|
+
{
|
|
1163
|
+
"percentThreshold": detail.percent_threshold,
|
|
1164
|
+
"cumulativeCostUSD": detail.cumulative_cost_usd,
|
|
1165
|
+
"marginalCostUSD": detail.marginal_cost_usd,
|
|
1166
|
+
"capturedAt": _iso_z(detail.captured_at),
|
|
1167
|
+
}
|
|
1168
|
+
for detail in row.detail
|
|
1169
|
+
],
|
|
1170
|
+
}
|
|
1171
|
+
for row in series.rows
|
|
1172
|
+
],
|
|
1173
|
+
}
|
|
1174
|
+
for series in data.series
|
|
1175
|
+
],
|
|
1176
|
+
} if data.quota_status != "unavailable" else None
|
|
1177
|
+
return {
|
|
1178
|
+
"asOf": _iso_z(data.as_of),
|
|
1179
|
+
"sections": [{
|
|
1180
|
+
"key": "quota-series",
|
|
1181
|
+
"status": data.quota_status,
|
|
1182
|
+
"data": section_data,
|
|
1183
|
+
"warnings": [
|
|
1184
|
+
{"code": warning.code, "message": warning.message}
|
|
1185
|
+
for warning in data.quota_warnings
|
|
1186
|
+
],
|
|
1187
|
+
}],
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _physical_totals(value: object) -> TokenTotals:
|
|
1192
|
+
if isinstance(value, SourceResult):
|
|
1193
|
+
if value.status == "unavailable" or value.data is None:
|
|
1194
|
+
return TokenTotals()
|
|
1195
|
+
value = value.data
|
|
1196
|
+
if isinstance(value, TokenTotals):
|
|
1197
|
+
return value
|
|
1198
|
+
if isinstance(value, (CodexProjectData, CodexRangeData, CodexReuseData)):
|
|
1199
|
+
return value.totals
|
|
1200
|
+
if isinstance(value, CodexDiffData):
|
|
1201
|
+
raise ValueError("diff has two windows and is not one physical accounting total")
|
|
1202
|
+
if isinstance(value, CodexReportData):
|
|
1203
|
+
raise ValueError("report quota series overlap and cannot be combined")
|
|
1204
|
+
if isinstance(value, dict):
|
|
1205
|
+
try:
|
|
1206
|
+
return TokenTotals(
|
|
1207
|
+
total_tokens=int(value.get("totalTokens", value.get("total_tokens", 0))),
|
|
1208
|
+
cost_usd=float(value.get("costUsd", value.get("cost_usd", 0.0))),
|
|
1209
|
+
)
|
|
1210
|
+
except (TypeError, ValueError) as exc:
|
|
1211
|
+
raise ValueError("invalid physical accounting totals") from exc
|
|
1212
|
+
raise TypeError("unsupported physical accounting value")
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def combine_physical_accounting(claude: object, codex: object) -> dict[str, object]:
|
|
1216
|
+
"""Combine only one already-deduplicated physical total per provider."""
|
|
1217
|
+
claude_totals = _physical_totals(claude)
|
|
1218
|
+
codex_totals = _physical_totals(codex)
|
|
1219
|
+
return {
|
|
1220
|
+
"costUsd": claude_totals.cost_usd + codex_totals.cost_usd,
|
|
1221
|
+
"totalTokens": claude_totals.total_tokens + codex_totals.total_tokens,
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _diff_combined_values(result: SourceResult[object]) -> tuple[TokenTotals, TokenTotals] | None:
|
|
1226
|
+
"""Return one provider's truthful A/B totals without flattening its wire."""
|
|
1227
|
+
if result.status == "unavailable" or result.data is None:
|
|
1228
|
+
return None
|
|
1229
|
+
if isinstance(result.data, CodexDiffData):
|
|
1230
|
+
return result.data.combined_a, result.data.combined_b
|
|
1231
|
+
if not isinstance(result.data, dict):
|
|
1232
|
+
raise TypeError("diff source data must retain a combined object")
|
|
1233
|
+
combined = result.data.get("combined")
|
|
1234
|
+
if not isinstance(combined, dict):
|
|
1235
|
+
raise ValueError("diff source data is missing combined accounting")
|
|
1236
|
+
|
|
1237
|
+
def totals_for(metric_name: str) -> tuple[float, float]:
|
|
1238
|
+
metric = combined.get(metric_name)
|
|
1239
|
+
if not isinstance(metric, dict):
|
|
1240
|
+
raise ValueError(f"diff source data is missing {metric_name}")
|
|
1241
|
+
try:
|
|
1242
|
+
return float(metric["a"]), float(metric["b"])
|
|
1243
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
1244
|
+
raise ValueError(f"diff source data has invalid {metric_name}") from exc
|
|
1245
|
+
|
|
1246
|
+
cost_a, cost_b = totals_for("cost_usd")
|
|
1247
|
+
tokens_a, tokens_b = totals_for("total_tokens")
|
|
1248
|
+
return (
|
|
1249
|
+
TokenTotals(cost_usd=cost_a, total_tokens=int(tokens_a)),
|
|
1250
|
+
TokenTotals(cost_usd=cost_b, total_tokens=int(tokens_b)),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def combine_diff_accounting(
|
|
1255
|
+
claude: SourceResult[object], codex: SourceResult[object],
|
|
1256
|
+
) -> dict[str, object]:
|
|
1257
|
+
"""Combine truthful per-window diff accounting without scalar coercion.
|
|
1258
|
+
|
|
1259
|
+
An unavailable source yields no inferred values; only the provider blocks
|
|
1260
|
+
with complete, truthful A/B accounting contribute to the all-source pair.
|
|
1261
|
+
"""
|
|
1262
|
+
totals = [
|
|
1263
|
+
values for values in (_diff_combined_values(claude), _diff_combined_values(codex))
|
|
1264
|
+
if values is not None
|
|
1265
|
+
]
|
|
1266
|
+
a_cost = math.fsum(values[0].cost_usd for values in totals)
|
|
1267
|
+
b_cost = math.fsum(values[1].cost_usd for values in totals)
|
|
1268
|
+
a_tokens = sum(values[0].total_tokens for values in totals)
|
|
1269
|
+
b_tokens = sum(values[1].total_tokens for values in totals)
|
|
1270
|
+
return {
|
|
1271
|
+
"cost_usd": {"a": a_cost, "b": b_cost, "delta": b_cost - a_cost},
|
|
1272
|
+
"total_tokens": {"a": a_tokens, "b": b_tokens, "delta": b_tokens - a_tokens},
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def source_result_wire(result: SourceResult[object], *, diff: bool = False) -> dict[str, object]:
|
|
1277
|
+
"""Render the exact direct-provider envelope for implemented pure kernels."""
|
|
1278
|
+
if result.data is None:
|
|
1279
|
+
data: object = None
|
|
1280
|
+
elif isinstance(result.data, CodexProjectData):
|
|
1281
|
+
data: object = {
|
|
1282
|
+
"rangeStart": _iso_z(result.data.range_start),
|
|
1283
|
+
"rangeEnd": _iso_z(result.data.range_end),
|
|
1284
|
+
"windowKind": result.data.window_kind,
|
|
1285
|
+
"totals": {"costUsd": result.data.totals.cost_usd, "totalTokens": result.data.totals.total_tokens},
|
|
1286
|
+
"projects": [
|
|
1287
|
+
_project_wire(row, include_breakdown=result.data.include_breakdown)
|
|
1288
|
+
for row in result.data.projects
|
|
1289
|
+
],
|
|
1290
|
+
}
|
|
1291
|
+
elif isinstance(result.data, CodexRangeData):
|
|
1292
|
+
data = _range_wire(result.data)
|
|
1293
|
+
elif isinstance(result.data, CodexReuseData):
|
|
1294
|
+
data = _reuse_wire(result.data)
|
|
1295
|
+
elif isinstance(result.data, CodexDiffData):
|
|
1296
|
+
data = _diff_wire(result.data)
|
|
1297
|
+
diff = True
|
|
1298
|
+
elif isinstance(result.data, CodexReportData):
|
|
1299
|
+
data = _report_wire(result.data)
|
|
1300
|
+
else:
|
|
1301
|
+
raise TypeError("unsupported source result data")
|
|
1302
|
+
warnings = [{"code": warning.code, "message": warning.message} for warning in result.warnings]
|
|
1303
|
+
if diff:
|
|
1304
|
+
return {
|
|
1305
|
+
"schema_version": 1,
|
|
1306
|
+
"source": result.source,
|
|
1307
|
+
"status": result.status,
|
|
1308
|
+
"data": data,
|
|
1309
|
+
"warnings": warnings,
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
"schemaVersion": 1,
|
|
1313
|
+
"source": result.source,
|
|
1314
|
+
"status": result.status,
|
|
1315
|
+
"data": data,
|
|
1316
|
+
"warnings": warnings,
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _source_block_wire(result: SourceResult[object], *, diff: bool) -> dict[str, object]:
|
|
1321
|
+
"""Return one source block without changing its provider-native data."""
|
|
1322
|
+
if result.source == "codex":
|
|
1323
|
+
direct = source_result_wire(result, diff=diff)
|
|
1324
|
+
return {
|
|
1325
|
+
"source": direct["source"],
|
|
1326
|
+
"status": direct["status"],
|
|
1327
|
+
"data": direct["data"],
|
|
1328
|
+
"warnings": direct["warnings"],
|
|
1329
|
+
}
|
|
1330
|
+
if result.source != "claude":
|
|
1331
|
+
raise ValueError("all-source results require claude and codex blocks")
|
|
1332
|
+
warnings = [{"code": warning.code, "message": warning.message} for warning in result.warnings]
|
|
1333
|
+
return {
|
|
1334
|
+
"source": "claude",
|
|
1335
|
+
"status": result.status,
|
|
1336
|
+
"data": result.data,
|
|
1337
|
+
"warnings": warnings,
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def all_source_result_wire(
|
|
1342
|
+
claude: SourceResult[object], codex: SourceResult[object], *,
|
|
1343
|
+
diff: bool = False, report: bool = False,
|
|
1344
|
+
) -> dict[str, object]:
|
|
1345
|
+
"""Compose separate provider blocks without flattening unlike semantics.
|
|
1346
|
+
|
|
1347
|
+
Only the dedicated physical accounting combiner may supply the all-source
|
|
1348
|
+
aggregate. Quota reports explicitly omit it because their logical-limit
|
|
1349
|
+
series can overlap the same physical accounting rows.
|
|
1350
|
+
"""
|
|
1351
|
+
if claude.source != "claude" or codex.source != "codex":
|
|
1352
|
+
raise ValueError("all-source results require claude then codex")
|
|
1353
|
+
sources = [
|
|
1354
|
+
_source_block_wire(claude, diff=diff),
|
|
1355
|
+
_source_block_wire(codex, diff=diff),
|
|
1356
|
+
]
|
|
1357
|
+
if diff:
|
|
1358
|
+
result: dict[str, object] = {
|
|
1359
|
+
"schema_version": 1,
|
|
1360
|
+
"source": "all",
|
|
1361
|
+
}
|
|
1362
|
+
if not report:
|
|
1363
|
+
result["combined"] = combine_diff_accounting(claude, codex)
|
|
1364
|
+
result["sources"] = sources
|
|
1365
|
+
result["warnings"] = []
|
|
1366
|
+
return result
|
|
1367
|
+
result = {
|
|
1368
|
+
"schemaVersion": 1,
|
|
1369
|
+
"source": "all",
|
|
1370
|
+
}
|
|
1371
|
+
if not report:
|
|
1372
|
+
result["combined"] = combine_physical_accounting(claude, codex)
|
|
1373
|
+
result["sources"] = sources
|
|
1374
|
+
result["warnings"] = []
|
|
1375
|
+
return result
|