cctally 1.82.1 → 1.83.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 +58 -0
- package/README.md +12 -5
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +912 -149
- package/bin/_cctally_config.py +43 -4
- package/bin/_cctally_core.py +933 -759
- package/bin/_cctally_dashboard.py +157 -47
- package/bin/_cctally_dashboard_cache_report.py +13 -6
- package/bin/_cctally_dashboard_conversation.py +1 -0
- package/bin/_cctally_dashboard_envelope.py +116 -8
- package/bin/_cctally_dashboard_share.py +50 -19
- package/bin/_cctally_dashboard_sources.py +223 -48
- package/bin/_cctally_db.py +605 -128
- package/bin/_cctally_doctor.py +413 -28
- package/bin/_cctally_five_hour.py +12 -5
- package/bin/_cctally_journal.py +2050 -156
- package/bin/_cctally_journal_repair.py +519 -0
- package/bin/_cctally_milestone_history.py +142 -56
- package/bin/_cctally_milestones.py +179 -111
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +24 -18
- package/bin/_cctally_quota.py +139 -25
- package/bin/_cctally_record.py +279 -108
- package/bin/_cctally_rederive.py +1052 -0
- package/bin/_cctally_reporting.py +58 -53
- package/bin/_cctally_setup.py +1 -0
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_statusline.py +11 -11
- package/bin/_cctally_store.py +1039 -31
- package/bin/_cctally_sync_week.py +17 -8
- package/bin/_cctally_tui.py +350 -44
- package/bin/_cctally_update.py +133 -8
- package/bin/_cctally_weekrefs.py +14 -0
- package/bin/_lib_aggregators.py +10 -6
- package/bin/_lib_cache_report.py +101 -9
- package/bin/_lib_codex_pools.py +82 -0
- package/bin/_lib_conversation_query.py +81 -33
- package/bin/_lib_dashboard_sources.py +75 -0
- package/bin/_lib_diff_kernel.py +28 -15
- package/bin/_lib_doctor.py +342 -4
- package/bin/_lib_journal.py +924 -2
- package/bin/_lib_jsonl.py +43 -14
- package/bin/_lib_pricing.py +140 -21
- package/bin/_lib_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
- package/dashboard/static/assets/index-D27EIHEI.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
|
@@ -21,7 +21,7 @@ from datetime import datetime as _datetime, timezone as _timezone
|
|
|
21
21
|
# Public surface (Plan 2): shipped in the npm tarball + brew formula + public
|
|
22
22
|
# mirror — imported by the dashboard's conversation endpoints at runtime.
|
|
23
23
|
|
|
24
|
-
from _lib_pricing import _calculate_entry_cost, _chip_for_model
|
|
24
|
+
from _lib_pricing import _calculate_entry_cost, _chip_for_model, claude_usage_dict
|
|
25
25
|
# #178: the on-demand load-full re-read helper re-stringifies a raw tool_result
|
|
26
26
|
# content block the same way the parser does at ingest — reuse the parser's
|
|
27
27
|
# _stringify so the full (un-capped) result text matches the cached/capped one.
|
|
@@ -546,16 +546,22 @@ def _iso_ms(ts):
|
|
|
546
546
|
return None
|
|
547
547
|
|
|
548
548
|
|
|
549
|
-
def _entry_cost(
|
|
549
|
+
def _entry_cost(
|
|
550
|
+
model, inp, out, cc, cr, cost_usd_raw, *, cc_1h, speed
|
|
551
|
+
) -> float:
|
|
550
552
|
"""Cost for one session_entries row via the shared pricing helper. Tokens →
|
|
551
553
|
the helper's usage dict. cost_usd_raw is passed as the optional override the
|
|
552
|
-
helper already understands (it is often NULL — never the primary source).
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
554
|
+
helper already understands (it is often NULL — never the primary source).
|
|
555
|
+
|
|
556
|
+
`cc_1h` (#195) is a REQUIRED keyword, inherited verbatim from
|
|
557
|
+
`claude_usage_dict`: this is a thin shim over the builder, so a default here
|
|
558
|
+
would re-open the exact hazard the builder's required keyword closes — a
|
|
559
|
+
caller that forgets the split does not raise, it silently prices every
|
|
560
|
+
1-hour cache write at the 5-minute rate. Pass an explicit None for a
|
|
561
|
+
genuinely unknown split; that reads as a declaration at the call site."""
|
|
562
|
+
usage = claude_usage_dict(
|
|
563
|
+
input_tokens=inp, output_tokens=out, cache_creation_tokens=cc,
|
|
564
|
+
cache_read_tokens=cr, cache_1h_tokens=cc_1h, speed=speed)
|
|
559
565
|
return _calculate_entry_cost(model or "", usage, cost_usd=cost_usd_raw)
|
|
560
566
|
|
|
561
567
|
|
|
@@ -576,26 +582,49 @@ _CACHE_FAILURE_CACHE_FLOOR = 20_000 # prior cache must be meaningful to "lo
|
|
|
576
582
|
_CACHE_FAILURE_CREATE_FLOOR = 20_000 # the re-creation must be substantial / real cost
|
|
577
583
|
|
|
578
584
|
|
|
579
|
-
def _cache_failure_wasted_usd(model, lost):
|
|
585
|
+
def _cache_failure_wasted_usd(model, lost, *, speed):
|
|
580
586
|
"""Marginal extra paid by re-creating `lost` previously-cached tokens at the
|
|
581
587
|
cache-WRITE rate instead of reading them at the cache-READ rate. Reuses the
|
|
582
588
|
pricing chokepoint `_calculate_entry_cost` (zero on unknown models — the
|
|
583
589
|
helper emits its own one-shot stderr warning, never raises). NEVER summed into
|
|
584
|
-
any cost-snapshot / budget / reconciled figure — a display-only estimate.
|
|
585
|
-
|
|
586
|
-
|
|
590
|
+
any cost-snapshot / budget / reconciled figure — a display-only estimate.
|
|
591
|
+
The lost-prefix subset has no authoritative mapping to the source row's
|
|
592
|
+
5m/1h write buckets, so this preserves the existing 5m estimate while
|
|
593
|
+
applying the retained effective speed tier."""
|
|
594
|
+
write = _calculate_entry_cost(
|
|
595
|
+
model or "",
|
|
596
|
+
claude_usage_dict(
|
|
597
|
+
cache_1h_tokens=None, speed=speed, cache_creation_tokens=lost
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
read = _calculate_entry_cost(
|
|
601
|
+
model or "",
|
|
602
|
+
claude_usage_dict(
|
|
603
|
+
cache_1h_tokens=None, speed=speed, cache_read_tokens=lost
|
|
604
|
+
),
|
|
605
|
+
)
|
|
587
606
|
return write - read
|
|
588
607
|
|
|
589
608
|
|
|
590
|
-
def _cache_read_saved_usd(model, cache_read):
|
|
609
|
+
def _cache_read_saved_usd(model, cache_read, *, speed):
|
|
591
610
|
"""Marginal USD the cache SAVED this turn: the `cache_read` prefix priced at
|
|
592
611
|
the full input rate minus its actual cache-READ rate. Display-only (same
|
|
593
612
|
caveat as `_cache_failure_wasted_usd`): NEVER summed into a cost-snapshot /
|
|
594
613
|
budget / reconciled figure. `input_tokens` and `cache_read_input_tokens` are
|
|
595
614
|
independent keys in the Claude `_calculate_entry_cost` (no subset
|
|
596
615
|
subtraction), so passing each alone yields the two rates cleanly."""
|
|
597
|
-
full = _calculate_entry_cost(
|
|
598
|
-
|
|
616
|
+
full = _calculate_entry_cost(
|
|
617
|
+
model or "",
|
|
618
|
+
claude_usage_dict(
|
|
619
|
+
cache_1h_tokens=None, speed=speed, input_tokens=cache_read
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
read = _calculate_entry_cost(
|
|
623
|
+
model or "",
|
|
624
|
+
claude_usage_dict(
|
|
625
|
+
cache_1h_tokens=None, speed=speed, cache_read_tokens=cache_read
|
|
626
|
+
),
|
|
627
|
+
)
|
|
599
628
|
return full - read
|
|
600
629
|
|
|
601
630
|
|
|
@@ -606,21 +635,24 @@ def _cache_read_saved_usd(model, cache_read):
|
|
|
606
635
|
# a document-ordered list of these and feed it to the ONE predicate below — the
|
|
607
636
|
# rule is implemented exactly once (U1, #217 S1).
|
|
608
637
|
class _CFEvent:
|
|
609
|
-
__slots__ = ("compaction", "key", "cc", "cr", "model")
|
|
638
|
+
__slots__ = ("compaction", "key", "cc", "cr", "model", "speed")
|
|
610
639
|
|
|
611
|
-
def __init__(
|
|
640
|
+
def __init__(
|
|
641
|
+
self, *, compaction=False, key=None, cc=0, cr=0, model=None, speed=None
|
|
642
|
+
):
|
|
612
643
|
self.compaction = compaction
|
|
613
644
|
self.key = key
|
|
614
645
|
self.cc = cc
|
|
615
646
|
self.cr = cr
|
|
616
647
|
self.model = model
|
|
648
|
+
self.speed = speed
|
|
617
649
|
|
|
618
650
|
|
|
619
651
|
def _iter_cache_failures(events):
|
|
620
652
|
"""The single cache-failure rule (spec §1), as a generator over a
|
|
621
653
|
document-ordered ``_CFEvent`` stream. Yields ``(index, prev_cached, lost,
|
|
622
|
-
model)`` for each FLAGGED assistant event — ``index`` is the event's
|
|
623
|
-
in ``events`` so a caller can map a flag back to its source item.
|
|
654
|
+
model, speed)`` for each FLAGGED assistant event — ``index`` is the event's
|
|
655
|
+
position in ``events`` so a caller can map a flag back to its source item.
|
|
624
656
|
|
|
625
657
|
Maintains a running-max of ``cache_read`` keyed by the event's ``key``
|
|
626
658
|
(``(subagent_key, model)`` — ``None`` subagent_key = main session). The key
|
|
@@ -660,7 +692,7 @@ def _iter_cache_failures(events):
|
|
|
660
692
|
and total > 0
|
|
661
693
|
and cc / total >= _CACHE_FAILURE_RECREATE_FRACTION):
|
|
662
694
|
lost = min(cc, max(0, rm - cr))
|
|
663
|
-
yield (i, rm, lost, ev.model)
|
|
695
|
+
yield (i, rm, lost, ev.model, ev.speed)
|
|
664
696
|
running_max[ev.key] = max(rm, cr)
|
|
665
697
|
|
|
666
698
|
|
|
@@ -695,7 +727,8 @@ def _cache_failure_events_from_items(items):
|
|
|
695
727
|
key=(it.get("subagent_key"), it.get("model")),
|
|
696
728
|
cc=tok.get("cache_creation", 0) or 0,
|
|
697
729
|
cr=tok.get("cache_read", 0) or 0,
|
|
698
|
-
model=it.get("model")
|
|
730
|
+
model=it.get("model"),
|
|
731
|
+
speed=it.get("_speed")))
|
|
699
732
|
sources.append(it)
|
|
700
733
|
return events, sources
|
|
701
734
|
|
|
@@ -719,11 +752,13 @@ def _stamp_cache_failures(items):
|
|
|
719
752
|
est_wasted_usd = write(lost) - read(lost)
|
|
720
753
|
"""
|
|
721
754
|
events, sources = _cache_failure_events_from_items(items)
|
|
722
|
-
for idx, prev_cached, lost, model in _iter_cache_failures(events):
|
|
755
|
+
for idx, prev_cached, lost, model, speed in _iter_cache_failures(events):
|
|
723
756
|
sources[idx]["cache_failure"] = {
|
|
724
757
|
"tokens_recreated": lost,
|
|
725
758
|
"prev_cached": prev_cached,
|
|
726
|
-
"est_wasted_usd": _cache_failure_wasted_usd(
|
|
759
|
+
"est_wasted_usd": _cache_failure_wasted_usd(
|
|
760
|
+
model, lost, speed=speed
|
|
761
|
+
),
|
|
727
762
|
}
|
|
728
763
|
|
|
729
764
|
|
|
@@ -901,10 +936,15 @@ def _turn_costs_for_keys(conn, keys):
|
|
|
901
936
|
cond = " OR ".join("(msg_id=? AND req_id=?)" for _ in chunk)
|
|
902
937
|
params = [v for pair in chunk for v in pair]
|
|
903
938
|
sql = ("SELECT msg_id, req_id, model, input_tokens, output_tokens, "
|
|
904
|
-
"cache_create_tokens, cache_read_tokens, cost_usd_raw "
|
|
939
|
+
"cache_create_tokens, cache_read_tokens, cost_usd_raw, "
|
|
940
|
+
"cache_create_1h_tokens, speed "
|
|
905
941
|
"FROM session_entries WHERE " + cond)
|
|
906
|
-
for m, r, model, inp, out, cc, cr, raw in conn.execute(
|
|
907
|
-
|
|
942
|
+
for m, r, model, inp, out, cc, cr, raw, cc1h, speed in conn.execute(
|
|
943
|
+
sql, params
|
|
944
|
+
):
|
|
945
|
+
costs[(m, r)] = _entry_cost(
|
|
946
|
+
model, inp, out, cc, cr, raw, cc_1h=cc1h, speed=speed
|
|
947
|
+
)
|
|
908
948
|
return costs
|
|
909
949
|
|
|
910
950
|
|
|
@@ -1539,7 +1579,7 @@ def _turn_cost_map(conn, turn_keys):
|
|
|
1539
1579
|
|
|
1540
1580
|
|
|
1541
1581
|
def _turn_usage_map(conn, turn_keys):
|
|
1542
|
-
"""{(msg_id, req_id): {"
|
|
1582
|
+
"""{(msg_id, req_id): {tokens..., "speed"}} for
|
|
1543
1583
|
the given non-null turn keys, read from the SAME deduped session_entries row
|
|
1544
1584
|
cost is computed from (#177). This is a SEPARATE sibling of _turn_cost_map —
|
|
1545
1585
|
that one returns a float and is also consumed by the search path
|
|
@@ -1557,11 +1597,12 @@ def _turn_usage_map(conn, turn_keys):
|
|
|
1557
1597
|
cond = " OR ".join("(msg_id=? AND req_id=?)" for _ in chunk)
|
|
1558
1598
|
params = [v for pair in chunk for v in pair]
|
|
1559
1599
|
sql = ("SELECT msg_id, req_id, input_tokens, output_tokens, "
|
|
1560
|
-
"cache_create_tokens, cache_read_tokens "
|
|
1600
|
+
"cache_create_tokens, cache_read_tokens, speed "
|
|
1561
1601
|
"FROM session_entries WHERE " + cond)
|
|
1562
|
-
for m, r, inp, out, cc, cr in conn.execute(sql, params):
|
|
1602
|
+
for m, r, inp, out, cc, cr, speed in conn.execute(sql, params):
|
|
1563
1603
|
usage[(m, r)] = {"input": inp or 0, "output": out or 0,
|
|
1564
|
-
"cache_creation": cc or 0, "cache_read": cr or 0
|
|
1604
|
+
"cache_creation": cc or 0, "cache_read": cr or 0,
|
|
1605
|
+
"speed": speed}
|
|
1565
1606
|
return usage
|
|
1566
1607
|
|
|
1567
1608
|
|
|
@@ -1981,7 +2022,11 @@ def _assemble_session(conn, session_id):
|
|
|
1981
2022
|
# key has no session_entries row (omitted, not zero-filled).
|
|
1982
2023
|
tok = usage.get((it["_msg_id"], it["_req_id"]))
|
|
1983
2024
|
if tok is not None:
|
|
1984
|
-
|
|
2025
|
+
# `speed` is an internal pricing input, not part of the public
|
|
2026
|
+
# token-count object. Keep it alongside the assembled item for
|
|
2027
|
+
# cache financials and strip it from reader page copies.
|
|
2028
|
+
it["tokens"] = {k: v for k, v in tok.items() if k != "speed"}
|
|
2029
|
+
it["_speed"] = tok.get("speed")
|
|
1985
2030
|
del it["_msg_id"]
|
|
1986
2031
|
del it["_req_id"]
|
|
1987
2032
|
it.pop("_has_prose", None)
|
|
@@ -2276,6 +2321,7 @@ def get_conversation(conn, session_id, *, after=None, before=None, tail=False,
|
|
|
2276
2321
|
patched = []
|
|
2277
2322
|
for it in page:
|
|
2278
2323
|
nit = dict(it)
|
|
2324
|
+
nit.pop("_speed", None)
|
|
2279
2325
|
nit["anchor"] = {**it["anchor"], "session_id": session_id}
|
|
2280
2326
|
if nit.get("text"):
|
|
2281
2327
|
nit["text"] = _strip_ansi(nit["text"])
|
|
@@ -2422,7 +2468,9 @@ def get_conversation_outline(conn, session_id):
|
|
|
2422
2468
|
tokens[k] += tok.get(k, 0)
|
|
2423
2469
|
cr_tokens = tok.get("cache_read", 0) or 0
|
|
2424
2470
|
if cr_tokens > 0:
|
|
2425
|
-
cache_saved += _cache_read_saved_usd(
|
|
2471
|
+
cache_saved += _cache_read_saved_usd(
|
|
2472
|
+
it.get("model"), cr_tokens, speed=it.get("_speed")
|
|
2473
|
+
)
|
|
2426
2474
|
# Copy the cache-failure marker onto the OutlineTurn exactly where
|
|
2427
2475
|
# tokens is copied (assistant-only, rides the same source row) and
|
|
2428
2476
|
# accumulate the session-level aggregate (spec §2).
|
|
@@ -17,6 +17,7 @@ PhysicalSource = Literal["claude", "codex"]
|
|
|
17
17
|
DashboardSelection = Literal["claude", "codex", "all"]
|
|
18
18
|
Availability = Literal["ok", "empty", "partial", "unavailable"]
|
|
19
19
|
Freshness = Literal["fresh", "stale"]
|
|
20
|
+
FreshnessDomain = Literal["hero", "quota", "sessions"]
|
|
20
21
|
CapabilityStatus = Literal[
|
|
21
22
|
"supported", "derived", "unavailable", "deferred", "not_applicable",
|
|
22
23
|
]
|
|
@@ -24,6 +25,7 @@ CapabilityStatus = Literal[
|
|
|
24
25
|
SOURCE_SCHEMA_VERSION = 1
|
|
25
26
|
DEFAULT_SOURCE = "claude"
|
|
26
27
|
SOURCE_ORDER = ("claude", "codex", "all")
|
|
28
|
+
SOURCE_FRESHNESS_DOMAINS = ("hero", "quota", "sessions")
|
|
27
29
|
|
|
28
30
|
_PHYSICAL_SOURCES = frozenset(("claude", "codex"))
|
|
29
31
|
_SELECTIONS = frozenset(SOURCE_ORDER)
|
|
@@ -109,10 +111,19 @@ class SourceDashboardState:
|
|
|
109
111
|
last_success_at: dt.datetime | None
|
|
110
112
|
capabilities: Mapping[str, CapabilityRecord]
|
|
111
113
|
data: Mapping[str, object] | None
|
|
114
|
+
# Domain freshness is orthogonal to provider-generation coherence. Legacy
|
|
115
|
+
# constructors may omit it; they deterministically inherit the provider
|
|
116
|
+
# value for every known domain.
|
|
117
|
+
domain_freshness: Mapping[str, Freshness] | None = None
|
|
112
118
|
# Immutable, server-only facts used to advance an idle presentation clock.
|
|
113
119
|
# They are deliberately separate from ``data`` so no internal accounting
|
|
114
120
|
# evidence becomes part of the public source-envelope contract.
|
|
115
121
|
clock_data: Mapping[str, object] | None = None
|
|
122
|
+
# Request-gated transcript content. This mapping is frozen with the source
|
|
123
|
+
# generation but is deliberately outside ``data``: source serialization
|
|
124
|
+
# publishes only ``data``, then the HTTP/SSE envelope layer injects a label
|
|
125
|
+
# into its request-local copies when that request's transcript gate is open.
|
|
126
|
+
private_session_labels: Mapping[str, str] | None = None
|
|
116
127
|
|
|
117
128
|
def __post_init__(self) -> None:
|
|
118
129
|
validate_dashboard_selection(self.source)
|
|
@@ -120,6 +131,16 @@ class SourceDashboardState:
|
|
|
120
131
|
raise ValueError("unsupported availability")
|
|
121
132
|
if self.freshness not in _FRESHNESS:
|
|
122
133
|
raise ValueError("unsupported freshness")
|
|
134
|
+
domain_freshness = (
|
|
135
|
+
{domain: self.freshness for domain in SOURCE_FRESHNESS_DOMAINS}
|
|
136
|
+
if self.domain_freshness is None else dict(self.domain_freshness)
|
|
137
|
+
)
|
|
138
|
+
if set(domain_freshness) != set(SOURCE_FRESHNESS_DOMAINS):
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"domain freshness must contain exactly hero, quota, and sessions"
|
|
141
|
+
)
|
|
142
|
+
if any(value not in _FRESHNESS for value in domain_freshness.values()):
|
|
143
|
+
raise ValueError("unsupported domain freshness")
|
|
123
144
|
if not isinstance(self.data_version, str):
|
|
124
145
|
raise ValueError("data_version must be a string")
|
|
125
146
|
if self.availability != "unavailable":
|
|
@@ -141,10 +162,20 @@ class SourceDashboardState:
|
|
|
141
162
|
raise ValueError("capabilities must contain CapabilityRecord values")
|
|
142
163
|
object.__setattr__(self, "warnings", warnings)
|
|
143
164
|
object.__setattr__(self, "capabilities", _freeze(capabilities))
|
|
165
|
+
object.__setattr__(self, "domain_freshness", _freeze(domain_freshness))
|
|
144
166
|
if self.data is not None:
|
|
145
167
|
object.__setattr__(self, "data", _freeze(self.data))
|
|
146
168
|
if self.clock_data is not None:
|
|
147
169
|
object.__setattr__(self, "clock_data", _freeze(self.clock_data))
|
|
170
|
+
if self.private_session_labels is not None:
|
|
171
|
+
private_session_labels = {
|
|
172
|
+
_nonempty_string(key, "private session label key"):
|
|
173
|
+
_nonempty_string(value, "private session label")
|
|
174
|
+
for key, value in self.private_session_labels.items()
|
|
175
|
+
}
|
|
176
|
+
object.__setattr__(
|
|
177
|
+
self, "private_session_labels", _freeze(private_session_labels),
|
|
178
|
+
)
|
|
148
179
|
|
|
149
180
|
|
|
150
181
|
@dataclass(frozen=True)
|
|
@@ -246,7 +277,11 @@ def degrade_source_state(
|
|
|
246
277
|
last_success_at=prior.last_success_at,
|
|
247
278
|
capabilities=prior.capabilities,
|
|
248
279
|
data=prior.data,
|
|
280
|
+
domain_freshness={
|
|
281
|
+
domain: "stale" for domain in SOURCE_FRESHNESS_DOMAINS
|
|
282
|
+
},
|
|
249
283
|
clock_data=prior.clock_data,
|
|
284
|
+
private_session_labels=prior.private_session_labels,
|
|
250
285
|
)
|
|
251
286
|
|
|
252
287
|
|
|
@@ -267,9 +302,28 @@ def unavailable_source_state(
|
|
|
267
302
|
last_success_at=None,
|
|
268
303
|
capabilities={},
|
|
269
304
|
data=None,
|
|
305
|
+
domain_freshness={
|
|
306
|
+
domain: "stale" for domain in SOURCE_FRESHNESS_DOMAINS
|
|
307
|
+
},
|
|
270
308
|
)
|
|
271
309
|
|
|
272
310
|
|
|
311
|
+
def source_domain_freshness(
|
|
312
|
+
state: SourceDashboardState,
|
|
313
|
+
domain: FreshnessDomain,
|
|
314
|
+
) -> Freshness:
|
|
315
|
+
"""Return one domain value with the frozen legacy-provider fallback."""
|
|
316
|
+
if domain not in SOURCE_FRESHNESS_DOMAINS:
|
|
317
|
+
raise ValueError("unsupported freshness domain")
|
|
318
|
+
mapping = getattr(state, "domain_freshness", None)
|
|
319
|
+
if isinstance(mapping, Mapping):
|
|
320
|
+
value = mapping.get(domain)
|
|
321
|
+
if value in _FRESHNESS:
|
|
322
|
+
return value
|
|
323
|
+
provider = getattr(state, "freshness", "stale")
|
|
324
|
+
return provider if provider in _FRESHNESS else "stale"
|
|
325
|
+
|
|
326
|
+
|
|
273
327
|
def _coherent_provider(state: SourceDashboardState) -> bool:
|
|
274
328
|
return (
|
|
275
329
|
state.availability in ("ok", "empty", "partial")
|
|
@@ -303,6 +357,8 @@ def _hero_cycle_is_stale(state: SourceDashboardState) -> bool:
|
|
|
303
357
|
``hero.cycle_freshness`` is additive and OMITTED while the cycle is fresh, so
|
|
304
358
|
this is false for every provider and every generation that predates #350.
|
|
305
359
|
"""
|
|
360
|
+
if source_domain_freshness(state, "hero") == "stale":
|
|
361
|
+
return True
|
|
306
362
|
if not isinstance(state.data, Mapping):
|
|
307
363
|
return False
|
|
308
364
|
hero = state.data.get("hero")
|
|
@@ -429,7 +485,15 @@ def compose_all_state(
|
|
|
429
485
|
version_material = json.dumps(
|
|
430
486
|
[
|
|
431
487
|
claude.data_version, claude.availability, claude.freshness,
|
|
488
|
+
[
|
|
489
|
+
source_domain_freshness(claude, domain)
|
|
490
|
+
for domain in SOURCE_FRESHNESS_DOMAINS
|
|
491
|
+
],
|
|
432
492
|
codex.data_version, codex.availability, codex.freshness,
|
|
493
|
+
[
|
|
494
|
+
source_domain_freshness(codex, domain)
|
|
495
|
+
for domain in SOURCE_FRESHNESS_DOMAINS
|
|
496
|
+
],
|
|
433
497
|
combined is not None,
|
|
434
498
|
],
|
|
435
499
|
separators=(",", ":"),
|
|
@@ -462,6 +526,17 @@ def compose_all_state(
|
|
|
462
526
|
"codex": codex.data,
|
|
463
527
|
},
|
|
464
528
|
},
|
|
529
|
+
domain_freshness={
|
|
530
|
+
domain: (
|
|
531
|
+
"fresh"
|
|
532
|
+
if all(
|
|
533
|
+
source_domain_freshness(state, domain) == "fresh"
|
|
534
|
+
for state in (claude, codex)
|
|
535
|
+
)
|
|
536
|
+
else "stale"
|
|
537
|
+
)
|
|
538
|
+
for domain in SOURCE_FRESHNESS_DOMAINS
|
|
539
|
+
},
|
|
465
540
|
)
|
|
466
541
|
|
|
467
542
|
|
package/bin/_lib_diff_kernel.py
CHANGED
|
@@ -110,6 +110,8 @@ def _load_lib(name: str):
|
|
|
110
110
|
|
|
111
111
|
_lib_pricing = _load_lib("_lib_pricing")
|
|
112
112
|
_calculate_entry_cost = _lib_pricing._calculate_entry_cost
|
|
113
|
+
# #195: the single construction point for every cost-feeding usage dict.
|
|
114
|
+
claude_usage_dict = _lib_pricing.claude_usage_dict
|
|
113
115
|
|
|
114
116
|
_lib_display_tz = _load_lib("_lib_display_tz")
|
|
115
117
|
_resolve_tz = _lib_display_tz._resolve_tz
|
|
@@ -512,12 +514,14 @@ def _diff_aggregate_overall(
|
|
|
512
514
|
continue
|
|
513
515
|
cost += _calculate_entry_cost(
|
|
514
516
|
e.model,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
517
|
+
claude_usage_dict( # #195 chokepoint
|
|
518
|
+
input_tokens=e.input_tokens,
|
|
519
|
+
output_tokens=e.output_tokens,
|
|
520
|
+
cache_creation_tokens=e.cache_creation_tokens,
|
|
521
|
+
cache_read_tokens=e.cache_read_tokens,
|
|
522
|
+
cache_1h_tokens=getattr(e, "cache_1h_tokens", None),
|
|
523
|
+
speed=getattr(e, "speed", None),
|
|
524
|
+
),
|
|
521
525
|
mode="auto",
|
|
522
526
|
cost_usd=e.cost_usd,
|
|
523
527
|
)
|
|
@@ -552,9 +556,12 @@ def _diff_aggregate_models(
|
|
|
552
556
|
})
|
|
553
557
|
b["cost"] += _calculate_entry_cost(
|
|
554
558
|
e.model,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
559
|
+
claude_usage_dict( # #195 chokepoint
|
|
560
|
+
input_tokens=e.input_tokens, output_tokens=e.output_tokens,
|
|
561
|
+
cache_creation_tokens=e.cache_creation_tokens,
|
|
562
|
+
cache_read_tokens=e.cache_read_tokens,
|
|
563
|
+
cache_1h_tokens=getattr(e, "cache_1h_tokens", None),
|
|
564
|
+
speed=getattr(e, "speed", None)),
|
|
558
565
|
mode="auto", cost_usd=e.cost_usd,
|
|
559
566
|
)
|
|
560
567
|
b["ti"] += e.input_tokens
|
|
@@ -593,9 +600,12 @@ def _diff_aggregate_projects(
|
|
|
593
600
|
})
|
|
594
601
|
b["cost"] += _calculate_entry_cost(
|
|
595
602
|
e.model,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
603
|
+
claude_usage_dict( # #195 chokepoint
|
|
604
|
+
input_tokens=e.input_tokens, output_tokens=e.output_tokens,
|
|
605
|
+
cache_creation_tokens=e.cache_creation_tokens,
|
|
606
|
+
cache_read_tokens=e.cache_read_tokens,
|
|
607
|
+
cache_1h_tokens=getattr(e, "cache_1h_tokens", None),
|
|
608
|
+
speed=getattr(e, "speed", None)),
|
|
599
609
|
mode="auto", cost_usd=e.cost_usd,
|
|
600
610
|
)
|
|
601
611
|
b["ti"] += e.input_tokens
|
|
@@ -639,9 +649,12 @@ def _diff_aggregate_cache(
|
|
|
639
649
|
continue
|
|
640
650
|
cost += _calculate_entry_cost(
|
|
641
651
|
e.model,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
652
|
+
claude_usage_dict( # #195 chokepoint
|
|
653
|
+
input_tokens=e.input_tokens, output_tokens=e.output_tokens,
|
|
654
|
+
cache_creation_tokens=e.cache_creation_tokens,
|
|
655
|
+
cache_read_tokens=e.cache_read_tokens,
|
|
656
|
+
cache_1h_tokens=getattr(e, "cache_1h_tokens", None),
|
|
657
|
+
speed=getattr(e, "speed", None)),
|
|
645
658
|
mode="auto", cost_usd=e.cost_usd,
|
|
646
659
|
)
|
|
647
660
|
tcr += e.cache_read_tokens
|