cctally 1.110.0 → 1.112.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -6
  3. package/bin/_cctally_cache.py +14 -7
  4. package/bin/_cctally_core.py +12 -1
  5. package/bin/_cctally_dashboard.py +60 -154
  6. package/bin/_cctally_dashboard_envelope.py +7 -6
  7. package/bin/_cctally_dashboard_sources.py +90 -55
  8. package/bin/_cctally_doctor.py +73 -27
  9. package/bin/_cctally_forecast.py +2 -1
  10. package/bin/_cctally_journal.py +147 -6
  11. package/bin/_cctally_milestones.py +29 -3
  12. package/bin/_cctally_quota.py +10 -1
  13. package/bin/_cctally_record.py +136 -4
  14. package/bin/_cctally_rederive.py +311 -8
  15. package/bin/_cctally_source_analytics.py +20 -8
  16. package/bin/_cctally_tui.py +546 -45
  17. package/bin/_cctally_update.py +420 -14
  18. package/bin/_lib_cache_report.py +7 -10
  19. package/bin/_lib_codex_conversation_query.py +40 -4
  20. package/bin/_lib_codex_metadata.py +57 -11
  21. package/bin/_lib_cost_provenance.py +303 -0
  22. package/bin/_lib_dashboard_sources.py +263 -28
  23. package/bin/_lib_doctor.py +2 -2
  24. package/bin/_lib_ingest_frontier.py +137 -38
  25. package/bin/_lib_pricing.py +24 -263
  26. package/bin/_lib_rederive.py +704 -36
  27. package/bin/_lib_snapshot_cache.py +332 -9
  28. package/bin/cctally +18 -5
  29. package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
  30. package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
  31. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
  32. package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
  33. package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
  34. package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
  35. package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
  36. package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
  37. package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
  38. package/dashboard/static/dashboard.html +2 -2
  39. package/package.json +2 -1
  40. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
  41. package/dashboard/static/assets/index-COhxe3vU.css +0 -1
@@ -17,17 +17,12 @@ Spec: docs/superpowers/specs/2026-05-13-bin-cctally-split-design.md
17
17
  """
18
18
  from __future__ import annotations
19
19
 
20
- import contextlib as _contextlib
21
- import contextvars as _contextvars
22
20
  import dataclasses
23
21
  import datetime as dt
24
22
  import re
25
23
  import sys
26
- import threading as _threading
27
24
  from typing import Any
28
25
 
29
- _sys = sys
30
-
31
26
 
32
27
  def _eprint(*args: Any) -> None:
33
28
  print(*args, file=sys.stderr)
@@ -68,7 +63,7 @@ def _chip_for_model(name: str) -> str:
68
63
  # fingerprint a store recorded and refuses a write from an older process. That
69
64
  # comparison is day-granular by construction, so two revisions sharing a date
70
65
  # compare equal and the older process is authorized to write.
71
- PRICING_SNAPSHOT_DATE = "2026-09-23"
66
+ PRICING_SNAPSHOT_DATE = "2026-09-29"
72
67
  PRICING_STALENESS_DAYS = 60 # release pre-flight WARNs past this age
73
68
 
74
69
 
@@ -405,6 +400,11 @@ PRICING_DRIFT_ALLOWLIST: list[dict] = [
405
400
  # MTok, $5 five-minute cache write, $0.20 cache read, and 2x fast rate.
406
401
  # Its 1M context uses standard rates throughout. The pricing fingerprint
407
402
  # advances to 2026-09-23 because another revision already used 2026-09-22.
403
+ # 2026-09-29: added claude-sonnet-5-5 at Anthropic's published $2/$10 per
404
+ # MTok, $2.50 five-minute cache write and $0.20 cache read, the same card
405
+ # as Sonnet 5 and the standard 0.1x read multiplier. 1M context at standard
406
+ # pricing, so NO above-200k tier, and no fast mode, so no fast multiplier.
407
+ # Verified against LiteLLM's anthropic-provider row on 2026-09-29.
408
408
  # Anthropic prices a cache WRITE by TTL: 1.25x base input for a 5-minute write,
409
409
  # 2x for a 1-hour write. Both WRITE multipliers are documented as applying
410
410
  # consistently across all supported models, so the 1h rate is DERIVED from
@@ -651,6 +651,13 @@ CLAUDE_MODEL_PRICING: dict[str, dict[str, Any]] = {
651
651
  "cache_creation_input_token_cost": 2.5e-06,
652
652
  "cache_read_input_token_cost": 2e-07,
653
653
  },
654
+ "claude-sonnet-5-5": {
655
+ # Source: https://platform.claude.com/docs/en/models/sonnet-5-5/overview
656
+ "input_cost_per_token": 2e-06,
657
+ "output_cost_per_token": 1e-05,
658
+ "cache_creation_input_token_cost": 2.5e-06,
659
+ "cache_read_input_token_cost": 2e-07,
660
+ },
654
661
  }
655
662
 
656
663
  # Anthropic fast-mode pricing is genuinely model-specific. Unsupported models
@@ -1448,36 +1455,20 @@ def _short_model_name(model: str) -> str:
1448
1455
 
1449
1456
 
1450
1457
  # ---------------------------------------------------------------------------
1451
- # #714 — the published pricing snapshot, and the reader that replaces it
1458
+ # The pricing snapshot every cost kernel reads.
1452
1459
  #
1453
- # Every CLI invocation re-imports this module, so only a long-lived process —
1454
- # the dashboard — can hold pricing an installer has already replaced on disk.
1455
- # Two rules shape everything below.
1456
- #
1457
- # A PARTIAL SWAP IS WORSE THAN NO SWAP. `PRICING_SNAPSHOT_DATE` is what gates
1458
- # the materialized-cost write refusal in `_cctally_cache`, so replacing the
1459
- # date alone clears the refusal and resumes writing cost computed from the old
1460
- # tables. A candidate is therefore validated whole and adopted whole, or it is
1461
- # rejected and the live snapshot is retained unchanged.
1462
- #
1463
- # NEVER `importlib.reload`. It executes arbitrary module code from a file an
1464
- # installer has just written; it mutates a live module dict incrementally, so a
1465
- # request in flight can see half a table; and it would not even fix the bug,
1466
- # because reloading this module rebinds none of the import-time copies listed
1467
- # in `PRICING_EXPORT_BINDINGS`. `ast.literal_eval` cannot execute code.
1468
-
1469
-
1470
- class PricingCandidateRejected(Exception):
1471
- """A candidate pricing file was not adopted. Carries the reason."""
1460
+ # Built once at import from the module globals above and never replaced. A
1461
+ # long-lived dashboard does not reload pricing in process: after a proven
1462
+ # install of a newer version it restarts itself (#868), and after a manual
1463
+ # install it keeps these tables until the user restarts it.
1472
1464
 
1473
1465
 
1474
1466
  class PricingSnapshot:
1475
1467
  """One complete, immutable pricing revision.
1476
1468
 
1477
- Every pricing-affecting value lives here — not only the two tables. A
1478
- tier threshold, the Codex fallback model, an alias or a fast multiplier
1479
- left behind by a partial swap prices part of an entry from the previous
1480
- revision, which is the same defect as a stale table and harder to see.
1469
+ Every pricing-affecting value lives here — not only the two tables: both
1470
+ tier thresholds, the Codex fallback model, the aliases, the cache-write
1471
+ multiplier and the fast multipliers.
1481
1472
 
1482
1473
  Hand-written frozen rather than ``@dataclasses.dataclass(frozen=True)``,
1483
1474
  for the reason `PricingFingerprintObservation` above states at length:
@@ -1522,81 +1513,6 @@ class PricingSnapshot:
1522
1513
  f"codex_models={len(self.codex_pricing)})")
1523
1514
 
1524
1515
 
1525
- #: The complete inventory of import-time copies, made explicit so it can be
1526
- #: tested rather than asserted. `(module name, attribute, field accessor)`.
1527
- #: `bin/cctally` copies the tables at import and both dashboard source modules
1528
- #: read those copies, so a swap that missed one would price from a stale table
1529
- #: with the refusal latch already cleared.
1530
- PRICING_EXPORT_BINDINGS = (
1531
- ("_lib_pricing", "PRICING_SNAPSHOT_DATE", lambda s: s.snapshot_date),
1532
- ("_lib_pricing", "CLAUDE_MODEL_PRICING", lambda s: s.claude_pricing),
1533
- ("_lib_pricing", "CODEX_MODEL_PRICING", lambda s: s.codex_pricing),
1534
- ("_lib_pricing", "CODEX_MODEL_ALIASES", lambda s: s.aliases),
1535
- ("_lib_pricing", "TIERED_THRESHOLD",
1536
- lambda s: s.tier_thresholds["claude"]),
1537
- ("_lib_pricing", "CODEX_TIERED_THRESHOLD",
1538
- lambda s: s.tier_thresholds["codex"]),
1539
- ("_lib_pricing", "CODEX_LEGACY_FALLBACK_MODEL", lambda s: s.fallback_model),
1540
- ("_lib_pricing", "CACHE_WRITE_1H_MULTIPLIER",
1541
- lambda s: s.cache_write_1h_multiplier),
1542
- ("_lib_pricing", "CLAUDE_FAST_MULTIPLIER_OVERRIDES",
1543
- lambda s: s.fast_multipliers["claude"]),
1544
- ("_lib_pricing", "CODEX_FAST_MULTIPLIER_OVERRIDES",
1545
- lambda s: s.fast_multipliers["codex"]),
1546
- ("_lib_pricing", "CODEX_FAST_MULTIPLIER_FALLBACK",
1547
- lambda s: s.fast_multipliers["codex_fallback"]),
1548
- ("cctally", "PRICING_SNAPSHOT_DATE", lambda s: s.snapshot_date),
1549
- ("cctally", "CLAUDE_MODEL_PRICING", lambda s: s.claude_pricing),
1550
- ("cctally", "CODEX_MODEL_PRICING", lambda s: s.codex_pricing),
1551
- ("cctally", "TIERED_THRESHOLD", lambda s: s.tier_thresholds["claude"]),
1552
- ("cctally", "CODEX_TIERED_THRESHOLD",
1553
- lambda s: s.tier_thresholds["codex"]),
1554
- ("cctally", "CODEX_LEGACY_FALLBACK_MODEL", lambda s: s.fallback_model),
1555
- ("cctally", "CACHE_WRITE_1H_MULTIPLIER",
1556
- lambda s: s.cache_write_1h_multiplier),
1557
- ("cctally", "CLAUDE_FAST_MULTIPLIER_OVERRIDES",
1558
- lambda s: s.fast_multipliers["claude"]),
1559
- ("cctally", "CODEX_FAST_MULTIPLIER_OVERRIDES",
1560
- lambda s: s.fast_multipliers["codex"]),
1561
- ("cctally", "CODEX_FAST_MULTIPLIER_FALLBACK",
1562
- lambda s: s.fast_multipliers["codex_fallback"]),
1563
- # `_cctally_cache` binds the date as a module global on purpose, so a test
1564
- # can monkeypatch it; that affordance is preserved and the binding is
1565
- # simply refreshed here too.
1566
- ("_cctally_cache", "PRICING_SNAPSHOT_DATE", lambda s: s.snapshot_date),
1567
- # `_lib_cache_report` unpacks the multiplier and the Claude tier threshold
1568
- # at import. The threshold used to be a literal `200_000` in that file with
1569
- # nothing keeping it equal to `TIERED_THRESHOLD` here, which made it a
1570
- # pricing value no reload could replace; it is read from this module now
1571
- # and refreshed through this entry.
1572
- ("_lib_cache_report", "CACHE_WRITE_1H_MULTIPLIER",
1573
- lambda s: s.cache_write_1h_multiplier),
1574
- ("_lib_cache_report", "DEFAULT_TIERED_THRESHOLD",
1575
- lambda s: s.tier_thresholds["claude"]),
1576
- )
1577
-
1578
- #: The names `read_pricing_snapshot_from_source` requires, each mapped to the
1579
- #: `PricingSnapshot` field it fills. A missing OR non-literal name rejects the
1580
- #: whole candidate.
1581
- _CANDIDATE_ASSIGNMENTS = (
1582
- "PRICING_SNAPSHOT_DATE",
1583
- "CLAUDE_MODEL_PRICING",
1584
- "CODEX_MODEL_PRICING",
1585
- "CODEX_MODEL_ALIASES",
1586
- "TIERED_THRESHOLD",
1587
- "CODEX_TIERED_THRESHOLD",
1588
- "CODEX_LEGACY_FALLBACK_MODEL",
1589
- "CACHE_WRITE_1H_MULTIPLIER",
1590
- "CLAUDE_FAST_MULTIPLIER_OVERRIDES",
1591
- "CODEX_FAST_MULTIPLIER_OVERRIDES",
1592
- "CODEX_FAST_MULTIPLIER_FALLBACK",
1593
- )
1594
-
1595
- _PRICING_PUBLISH_LOCK = _threading.Lock()
1596
- _PRICING_CONTEXT = _contextvars.ContextVar("cctally_pricing_snapshot",
1597
- default=None)
1598
-
1599
-
1600
1516
  def _snapshot_from_module_globals() -> PricingSnapshot:
1601
1517
  return PricingSnapshot(
1602
1518
  snapshot_date=PRICING_SNAPSHOT_DATE,
@@ -1619,161 +1535,6 @@ _LIVE_PRICING_SNAPSHOT = _snapshot_from_module_globals()
1619
1535
 
1620
1536
 
1621
1537
  def current_pricing_snapshot() -> PricingSnapshot:
1622
- """The snapshot this caller must price from.
1623
-
1624
- A caller inside `pricing_snapshot_context()` keeps the revision it entered
1625
- with, so a request, a dashboard snapshot build or a sync pass already
1626
- under way is never assembled from two revisions. Everyone else reads the
1627
- live pointer, which is one immutable object replaced by one assignment.
1628
- """
1629
- captured = _PRICING_CONTEXT.get()
1630
- return _LIVE_PRICING_SNAPSHOT if captured is None else captured
1631
-
1632
-
1633
- @_contextlib.contextmanager
1634
- def pricing_snapshot_context(snapshot: "PricingSnapshot | None" = None):
1635
- """Pin one revision for the duration of a request, build or sync pass."""
1636
- pinned = current_pricing_snapshot() if snapshot is None else snapshot
1637
- token = _PRICING_CONTEXT.set(pinned)
1638
- try:
1639
- yield pinned
1640
- finally:
1641
- _PRICING_CONTEXT.reset(token)
1642
-
1643
-
1644
- def publish_pricing_snapshot(snapshot: PricingSnapshot) -> None:
1645
- """Swap the live pointer and refresh every import-time copy.
1646
-
1647
- The pointer swap is one assignment of one frozen object, so a concurrent
1648
- reader observes the old revision whole or the new one whole. The binding
1649
- refresh that follows exists for call sites that still read a copied name;
1650
- every cost kernel in this module reads `current_pricing_snapshot()`
1651
- instead, so correctness does not depend on the refresh reaching a module
1652
- that has not been imported.
1653
- """
1654
- global _LIVE_PRICING_SNAPSHOT
1655
- with _PRICING_PUBLISH_LOCK:
1656
- _LIVE_PRICING_SNAPSHOT = snapshot
1657
- for module_name, attr, accessor in PRICING_EXPORT_BINDINGS:
1658
- module = _sys.modules.get(module_name)
1659
- if module is None:
1660
- continue
1661
- try:
1662
- setattr(module, attr, accessor(snapshot))
1663
- except Exception: # noqa: BLE001
1664
- # A module that refuses an attribute set must not stop the
1665
- # rest of the inventory being refreshed.
1666
- _eprint(f"[pricing] could not refresh {module_name}.{attr}")
1667
-
1668
-
1669
- def adopt_pricing_candidate(candidate: PricingSnapshot, *,
1670
- force: bool = False) -> bool:
1671
- """Publish `candidate` iff it is strictly newer than the live snapshot.
1672
-
1673
- `_lib_pricing` documents that a pricing revision always ADVANCES
1674
- `PRICING_SNAPSHOT_DATE`, so a candidate that does not is a rollback, a
1675
- corrupt read or the same revision read twice; each of those is a reason to
1676
- keep what is running. `force` exists for tests restoring a captured
1677
- snapshot and for nothing else.
1678
- """
1679
- if force:
1680
- publish_pricing_snapshot(candidate)
1681
- return True
1682
- live = _LIVE_PRICING_SNAPSHOT
1683
- new_date = parse_pricing_fingerprint(candidate.snapshot_date)
1684
- live_date = parse_pricing_fingerprint(live.snapshot_date)
1685
- if new_date is None:
1686
- return False
1687
- if live_date is not None and new_date <= live_date:
1688
- return False
1689
- publish_pricing_snapshot(candidate)
1690
- return True
1691
-
1692
-
1693
- def _file_signature(path) -> tuple:
1694
- st = path.stat()
1695
- return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
1696
-
1697
-
1698
- def read_pricing_snapshot_from_source(path) -> PricingSnapshot:
1699
- """Build a candidate from the literal assignments in a pricing file.
1700
-
1701
- `ast.parse` plus `ast.literal_eval`, never `importlib.reload` and never
1702
- `exec`: the file is one an installer has just written and this process
1703
- must not run it.
1704
-
1705
- Raises `PricingCandidateRejected` — never returns a partial snapshot — on
1706
- a missing name, a name whose value is not a literal, a date that does not
1707
- parse, or a file whose inode/size/mtime signature changes while it is
1708
- being read, which is an installer replacing it mid-parse.
1709
- """
1710
- import ast
1711
- import pathlib as _pathlib
1712
-
1713
- path = _pathlib.Path(path)
1714
- try:
1715
- before = _file_signature(path)
1716
- source = path.read_bytes()
1717
- after = _file_signature(path)
1718
- except OSError as exc:
1719
- raise PricingCandidateRejected(
1720
- f"pricing candidate could not be read: {exc}") from exc
1721
- if before != after:
1722
- raise PricingCandidateRejected(
1723
- "pricing candidate signature changed while it was being read")
1724
-
1725
- try:
1726
- tree = ast.parse(source.decode("utf-8"), filename=str(path))
1727
- except (SyntaxError, UnicodeDecodeError) as exc:
1728
- raise PricingCandidateRejected(
1729
- f"pricing candidate does not parse: {exc}") from exc
1730
-
1731
- found: "dict[str, Any]" = {}
1732
- for node in tree.body:
1733
- targets = []
1734
- if isinstance(node, ast.Assign):
1735
- targets = [t for t in node.targets if isinstance(t, ast.Name)]
1736
- value = node.value
1737
- elif isinstance(node, ast.AnnAssign) and isinstance(node.target,
1738
- ast.Name):
1739
- targets = [node.target]
1740
- value = node.value
1741
- else:
1742
- continue
1743
- for target in targets:
1744
- if target.id not in _CANDIDATE_ASSIGNMENTS or value is None:
1745
- continue
1746
- try:
1747
- found[target.id] = ast.literal_eval(value)
1748
- except (ValueError, SyntaxError, TypeError) as exc:
1749
- raise PricingCandidateRejected(
1750
- f"{target.id} is no longer a literal, so the in-process "
1751
- f"pricing reload cannot read it: {exc}") from exc
1752
-
1753
- missing = [name for name in _CANDIDATE_ASSIGNMENTS if name not in found]
1754
- if missing:
1755
- raise PricingCandidateRejected(
1756
- "pricing candidate is incomplete; adopting a partial revision "
1757
- "would clear the write refusal and price from the old tables. "
1758
- f"Missing: {', '.join(missing)}")
1759
-
1760
- if parse_pricing_fingerprint(found["PRICING_SNAPSHOT_DATE"]) is None:
1761
- raise PricingCandidateRejected(
1762
- "pricing candidate has no orderable PRICING_SNAPSHOT_DATE: "
1763
- f"{found['PRICING_SNAPSHOT_DATE']!r}")
1764
-
1765
- return PricingSnapshot(
1766
- snapshot_date=found["PRICING_SNAPSHOT_DATE"],
1767
- claude_pricing=found["CLAUDE_MODEL_PRICING"],
1768
- codex_pricing=found["CODEX_MODEL_PRICING"],
1769
- aliases=found["CODEX_MODEL_ALIASES"],
1770
- tier_thresholds={"claude": found["TIERED_THRESHOLD"],
1771
- "codex": found["CODEX_TIERED_THRESHOLD"]},
1772
- fallback_model=found["CODEX_LEGACY_FALLBACK_MODEL"],
1773
- cache_write_1h_multiplier=found["CACHE_WRITE_1H_MULTIPLIER"],
1774
- fast_multipliers={
1775
- "claude": found["CLAUDE_FAST_MULTIPLIER_OVERRIDES"],
1776
- "codex": found["CODEX_FAST_MULTIPLIER_OVERRIDES"],
1777
- "codex_fallback": found["CODEX_FAST_MULTIPLIER_FALLBACK"],
1778
- },
1779
- )
1538
+ """The snapshot every cost kernel prices from; it never changes after
1539
+ import."""
1540
+ return _LIVE_PRICING_SNAPSHOT