cctally 1.113.0 → 1.115.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 (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
package/bin/_lib_perf.py CHANGED
@@ -149,7 +149,8 @@ def _stack():
149
149
 
150
150
 
151
151
  class Phase:
152
- __slots__ = ("name", "elapsed_ms", "count", "meta", "children", "_start", "_stack")
152
+ __slots__ = ("name", "elapsed_ms", "count", "meta", "children", "_start",
153
+ "_stack", "write_bytes", "_write_start")
153
154
 
154
155
  def __init__(self, name, stack):
155
156
  self.name = name
@@ -159,6 +160,8 @@ class Phase:
159
160
  self.children = []
160
161
  self._start = 0.0
161
162
  self._stack = stack
163
+ self.write_bytes = None
164
+ self._write_start = None
162
165
 
163
166
  def set_count(self, n):
164
167
  self.count = int(n)
@@ -169,12 +172,17 @@ class Phase:
169
172
  self.meta.update(kw)
170
173
 
171
174
  def __enter__(self):
175
+ self._write_start = _write_counter_value()
172
176
  self._start = time.perf_counter()
173
177
  self._stack.append(self)
174
178
  return self
175
179
 
176
180
  def __exit__(self, *exc):
177
181
  self.elapsed_ms = (time.perf_counter() - self._start) * 1000.0
182
+ end = _write_counter_value()
183
+ if (self._write_start is not None and end is not None
184
+ and end >= self._write_start):
185
+ self.write_bytes = end - self._write_start
178
186
  # Identity-aware unwind. If a nested phase leaked (its __exit__ was
179
187
  # skipped — e.g. an exception escaped a manually CM-bracketed region),
180
188
  # drop the leaked frames sitting above us so we never append a phase to
@@ -203,6 +211,8 @@ class Phase:
203
211
  d["count"] = self.count
204
212
  if self.meta:
205
213
  d["meta"] = dict(self.meta)
214
+ if self.write_bytes is not None:
215
+ d["write_bytes"] = self.write_bytes
206
216
  if self.children:
207
217
  d["children"] = [c.to_dict() for c in self.children]
208
218
  return d
@@ -227,6 +237,22 @@ class _NullPhase:
227
237
  _NULL_PHASE = _NullPhase()
228
238
 
229
239
 
240
+ def _write_counter_value():
241
+ """The process write counter for an armed phase (#901 §5.5), or None.
242
+
243
+ Deep-phase samples are optional: they ride an armed trace only, and the
244
+ always-on totals never depend on them. The counter module is used only
245
+ when the process already loaded it, so this stays stdlib-only."""
246
+ module = sys.modules.get("_lib_write_io")
247
+ if module is None:
248
+ return None
249
+ try:
250
+ reading = module.read_counter()
251
+ except Exception: # noqa: BLE001 — a diagnostic must not raise
252
+ return None
253
+ return reading.value if reading.status == "ok" else None
254
+
255
+
230
256
  def phase(name):
231
257
  # Per-root capture (#583 S1 §2.2). `_ENABLED` is process-global and HTTP
232
258
  # handlers open their own roots on their own threads, so a flip landing
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import dataclasses
21
21
  import datetime as dt
22
+ import math
22
23
  import re
23
24
  import sys
24
25
  from typing import Any
@@ -30,6 +31,34 @@ def _eprint(*args: Any) -> None:
30
31
 
31
32
  TIERED_THRESHOLD = 200_000
32
33
 
34
+ # #929: Claude Haiku 5.5 is priced by prompt length. A card that declares
35
+ # `whole_request_prompt_threshold_tokens` selects ONE card per request from its
36
+ # prompt (input + flat cache creation + cache read): over the threshold every
37
+ # class of that request, output included, bills at the `_above_100k_tokens`
38
+ # rates. This is a different mechanism from the per-class MARGINAL
39
+ # `_above_200k_tokens` split older Sonnet cards carry, and a card never mixes
40
+ # the two. A legacy card carries neither the marker nor any 100K field.
41
+ CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY = "whole_request_prompt_threshold_tokens"
42
+ CLAUDE_WHOLE_REQUEST_THRESHOLD = 100_000
43
+ CLAUDE_WHOLE_REQUEST_TIER_FIELDS = (
44
+ ("input_cost_per_token", "input_cost_per_token_above_100k_tokens"),
45
+ ("output_cost_per_token", "output_cost_per_token_above_100k_tokens"),
46
+ ("cache_creation_input_token_cost",
47
+ "cache_creation_input_token_cost_above_100k_tokens"),
48
+ ("cache_read_input_token_cost",
49
+ "cache_read_input_token_cost_above_100k_tokens"),
50
+ )
51
+ CLAUDE_WHOLE_REQUEST_TIER_SUFFIX = "_above_100k_tokens"
52
+ # The marker plus the four recognised tier fields. A card sharing none of them
53
+ # is legacy; the hot path decides that with one `isdisjoint` call. A card that
54
+ # carries some OTHER `_above_100k_tokens` field is malformed: the selector
55
+ # refuses it when the card is otherwise whole-request, and
56
+ # `check_table_shapes` refuses it on any card (`claude_whole_request_
57
+ # declaration_problems`), so it can never silently price at the base card.
58
+ _CLAUDE_WHOLE_REQUEST_KEYS = frozenset(
59
+ (CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY,
60
+ *(tier for _, tier in CLAUDE_WHOLE_REQUEST_TIER_FIELDS)))
61
+
33
62
 
34
63
  def _chip_for_model(name: str) -> str:
35
64
  """Bucket a canonical model id into a small chip palette.
@@ -63,7 +92,12 @@ def _chip_for_model(name: str) -> str:
63
92
  # fingerprint a store recorded and refuses a write from an older process. That
64
93
  # comparison is day-granular by construction, so two revisions sharing a date
65
94
  # compare equal and the older process is authorized to write.
66
- PRICING_SNAPSHOT_DATE = "2026-09-30"
95
+ #
96
+ # A sync of this date or of either table also changes the dynamic-SQL shape
97
+ # pinned in `bin/_lib_detector_access_registry.py`, so its registry test fails
98
+ # until the affected pins are re-pinned through the reviewed procedure in that
99
+ # module's docstring. Bulk regeneration of the pins alone is not acceptance.
100
+ PRICING_SNAPSHOT_DATE = "2026-10-14"
67
101
  PRICING_STALENESS_DAYS = 60 # release pre-flight WARNs past this age
68
102
 
69
103
 
@@ -517,6 +551,21 @@ CLAUDE_MODEL_PRICING: dict[str, dict[str, Any]] = {
517
551
  "cache_creation_input_token_cost": 1.25e-06,
518
552
  "cache_read_input_token_cost": 1e-07,
519
553
  },
554
+ "claude-haiku-5-5": {
555
+ # Source: https://platform.claude.com/docs/en/models/haiku-5-5/overview
556
+ # Priced by prompt length (#929): over 100,000 prompt tokens the WHOLE
557
+ # request bills at the _above_100k_tokens card. The 1h write stays
558
+ # derived (selected input x CACHE_WRITE_1H_MULTIPLIER).
559
+ "input_cost_per_token": 1e-07,
560
+ "output_cost_per_token": 5e-07,
561
+ "cache_creation_input_token_cost": 1.25e-07,
562
+ "cache_read_input_token_cost": 1e-08,
563
+ "input_cost_per_token_above_100k_tokens": 5e-07,
564
+ "output_cost_per_token_above_100k_tokens": 2.5e-06,
565
+ "cache_creation_input_token_cost_above_100k_tokens": 6.25e-07,
566
+ "cache_read_input_token_cost_above_100k_tokens": 5e-08,
567
+ "whole_request_prompt_threshold_tokens": 100_000,
568
+ },
520
569
  "claude-mythos-5": {
521
570
  "input_cost_per_token": 1e-05,
522
571
  "output_cost_per_token": 5e-05,
@@ -656,7 +705,7 @@ CLAUDE_MODEL_PRICING: dict[str, dict[str, Any]] = {
656
705
  "input_cost_per_token": 2e-06,
657
706
  "output_cost_per_token": 1e-05,
658
707
  "cache_creation_input_token_cost": 2.5e-06,
659
- "cache_read_input_token_cost": 2e-07,
708
+ "cache_read_input_token_cost": 1e-07,
660
709
  },
661
710
  }
662
711
 
@@ -1274,6 +1323,118 @@ def claude_usage_dict(*, cache_1h_tokens, speed, input_tokens=0, output_tokens=0
1274
1323
  return usage
1275
1324
 
1276
1325
 
1326
+ def _finite_positive_rate(value) -> bool:
1327
+ return (isinstance(value, (int, float)) and not isinstance(value, bool)
1328
+ and math.isfinite(value) and value > 0)
1329
+
1330
+
1331
+ def _is_whole_request_card(card) -> bool:
1332
+ """True when ``card`` declares (or half-declares) a whole-request tier:
1333
+ it carries the marker or one of the four recognised tier fields. This is
1334
+ on every Claude request's pricing path, so it is one set operation; the
1335
+ scan for unrecognised ``_above_100k_tokens`` fields lives in
1336
+ ``claude_whole_request_declaration_problems``."""
1337
+ return not _CLAUDE_WHOLE_REQUEST_KEYS.isdisjoint(card)
1338
+
1339
+
1340
+ def _unrecognised_whole_request_fields(card) -> list:
1341
+ return sorted(k for k in card
1342
+ if k.endswith(CLAUDE_WHOLE_REQUEST_TIER_SUFFIX)
1343
+ and k not in _CLAUDE_WHOLE_REQUEST_KEYS)
1344
+
1345
+
1346
+ def claude_whole_request_declaration_problems(card) -> list:
1347
+ """Every reason ``card``'s whole-request declaration is malformed (#929).
1348
+
1349
+ ``[]`` for a legacy card (neither the marker nor any
1350
+ ``_above_100k_tokens`` field) and for a well-formed declaration. Shared by
1351
+ the selector and ``check_table_shapes`` so the two can never disagree
1352
+ about what is well formed. Any ``_above_100k_tokens`` field other than the
1353
+ four recognised ones is a problem, with or without the marker.
1354
+ """
1355
+ unrecognised = _unrecognised_whole_request_fields(card)
1356
+ if not unrecognised and not _is_whole_request_card(card):
1357
+ return []
1358
+ problems = []
1359
+ key = CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY
1360
+ threshold = card.get(key)
1361
+ if key not in card:
1362
+ problems.append(f"missing {key}")
1363
+ elif (isinstance(threshold, bool) or not isinstance(threshold, int)
1364
+ or threshold != CLAUDE_WHOLE_REQUEST_THRESHOLD):
1365
+ problems.append(
1366
+ f"{key} must be the integer {CLAUDE_WHOLE_REQUEST_THRESHOLD} the "
1367
+ f"_above_100k_tokens fields name (got {threshold!r})")
1368
+ for _, tier in CLAUDE_WHOLE_REQUEST_TIER_FIELDS:
1369
+ if tier not in card:
1370
+ problems.append(f"missing {tier}")
1371
+ elif not _finite_positive_rate(card[tier]):
1372
+ problems.append(
1373
+ f"{tier} not a finite positive number ({card[tier]!r})")
1374
+ mixed = sorted(k for k in card if k.endswith("_above_200k_tokens"))
1375
+ if mixed:
1376
+ problems.append("mixes whole-request and marginal tiers: "
1377
+ + ", ".join(mixed))
1378
+ if unrecognised:
1379
+ problems.append("unrecognised _above_100k_tokens fields: "
1380
+ + ", ".join(unrecognised))
1381
+ return problems
1382
+
1383
+
1384
+ def _select_claude_request_card(card, *, input_tokens, cache_creation_tokens,
1385
+ cache_read_tokens, prompt_tokens_for_tier=None):
1386
+ """The card that prices ONE request (#929). Pure: never writes ``card``.
1387
+
1388
+ A legacy card (neither the marker nor any of the four recognised tier
1389
+ fields) is returned unchanged (the same object) without reading a count.
1390
+ A whole-request card must be well formed, which includes carrying no
1391
+ unrecognised ``_above_100k_tokens`` field, or this raises
1392
+ ``ValueError``. Its prompt is ``prompt_tokens_for_tier`` when given (a
1393
+ caller pricing a synthetic subset of a real request), else input + FLAT
1394
+ cache creation + cache read; output and the 1h sub-split never count. At or
1395
+ under the threshold the stored card itself is returned; over it, a NEW dict
1396
+ whose four base slots hold the higher rates and which carries neither the
1397
+ marker nor any tier field.
1398
+ """
1399
+ if not _is_whole_request_card(card):
1400
+ return card
1401
+ problems = claude_whole_request_declaration_problems(card)
1402
+ if problems:
1403
+ raise ValueError("malformed whole-request pricing declaration: "
1404
+ + "; ".join(problems))
1405
+ if prompt_tokens_for_tier is None:
1406
+ prompt = ((input_tokens or 0) + (cache_creation_tokens or 0)
1407
+ + (cache_read_tokens or 0))
1408
+ else:
1409
+ prompt = prompt_tokens_for_tier
1410
+ if prompt <= card[CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY]:
1411
+ return card
1412
+ # No marker and no `_above_100k_tokens` field survives into the selected
1413
+ # card (a well-formed card carries no unrecognised one, but the filter
1414
+ # does not rely on that).
1415
+ selected = {k: v for k, v in card.items()
1416
+ if k != CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY
1417
+ and not k.endswith(CLAUDE_WHOLE_REQUEST_TIER_SUFFIX)}
1418
+ for base, tier in CLAUDE_WHOLE_REQUEST_TIER_FIELDS:
1419
+ selected[base] = card[tier]
1420
+ return selected
1421
+
1422
+
1423
+ def _whole_request_cache_create_cost(card, flat, h_raw) -> float:
1424
+ """Cache-creation USD on a whole-request card: direct flat arithmetic
1425
+ (#929 spec §5), never the legacy 200K band partition, which is not
1426
+ float-identical to it."""
1427
+ if flat <= 0:
1428
+ return 0.0
1429
+ rate_5m = card.get("cache_creation_input_token_cost", 0.0)
1430
+ h = 0 if h_raw is None else max(0, min(int(h_raw), flat))
1431
+ if h == 0:
1432
+ return flat * rate_5m
1433
+ rate_1h = (card.get("input_cost_per_token", 0.0)
1434
+ * current_pricing_snapshot().cache_write_1h_multiplier)
1435
+ return (flat - h) * rate_5m + h * rate_1h
1436
+
1437
+
1277
1438
  def _cache_create_cost(pricing: dict, flat: int, h_raw, tiered) -> float:
1278
1439
  """USD for one entry's cache-CREATION tokens, priced by TTL (#195).
1279
1440
 
@@ -1320,8 +1481,15 @@ def _calculate_entry_cost(
1320
1481
  usage: dict[str, Any],
1321
1482
  mode: str = "auto",
1322
1483
  cost_usd: float | None = None,
1484
+ *,
1485
+ prompt_tokens_for_tier: int | None = None,
1323
1486
  ) -> float:
1324
- """Calculate USD cost for a single API call entry."""
1487
+ """Calculate USD cost for a single API call entry.
1488
+
1489
+ ``prompt_tokens_for_tier`` only feeds whole-request card selection (#929):
1490
+ a caller pricing a synthetic subset of a real request passes that
1491
+ request's real prompt total so the subset bills at the request's card.
1492
+ """
1325
1493
  if mode == "display":
1326
1494
  return cost_usd if cost_usd is not None else 0.0
1327
1495
  if mode == "auto" and cost_usd is not None:
@@ -1330,6 +1498,18 @@ def _calculate_entry_cost(
1330
1498
  pricing = _resolve_model_pricing(model)
1331
1499
  if pricing is None:
1332
1500
  return 0.0
1501
+ # #929: a whole-request card selects ONE card for this request. A legacy
1502
+ # card skips selection entirely and runs the pre-#929 expressions below
1503
+ # byte-for-byte (the selector would return it unchanged anyway).
1504
+ whole_request = _is_whole_request_card(pricing)
1505
+ if whole_request:
1506
+ pricing = _select_claude_request_card(
1507
+ pricing,
1508
+ input_tokens=usage.get("input_tokens", 0),
1509
+ cache_creation_tokens=usage.get("cache_creation_input_tokens", 0),
1510
+ cache_read_tokens=usage.get("cache_read_input_tokens", 0),
1511
+ prompt_tokens_for_tier=prompt_tokens_for_tier,
1512
+ )
1333
1513
 
1334
1514
  def _tiered(tokens: int, base_key: str, tiered_key: str) -> float:
1335
1515
  base_rate = pricing.get(base_key, 0.0)
@@ -1361,12 +1541,22 @@ def _calculate_entry_cost(
1361
1541
  # arithmetic or comparison, and the `h_raw` side does its own `int()`
1362
1542
  # inside the clamp (which IS load-bearing there: `min(int(h_raw), flat)`
1363
1543
  # is what pins the 1h portion to whole tokens).
1364
- cache_create_cost = _cache_create_cost(
1365
- pricing,
1366
- usage.get("cache_creation_input_tokens", 0),
1367
- usage.get("cache_creation_1h_input_tokens"),
1368
- _tiered,
1369
- )
1544
+ if not whole_request:
1545
+ cache_create_cost = _cache_create_cost(
1546
+ pricing,
1547
+ usage.get("cache_creation_input_tokens", 0),
1548
+ usage.get("cache_creation_1h_input_tokens"),
1549
+ _tiered,
1550
+ )
1551
+ else:
1552
+ # The selected card has no `_above_200k_tokens` key, so `_tiered`
1553
+ # prices input, output and cache read as tokens x selected rate; cache
1554
+ # creation takes the direct flat form, not the 200K band partition.
1555
+ cache_create_cost = _whole_request_cache_create_cost(
1556
+ pricing,
1557
+ usage.get("cache_creation_input_tokens", 0),
1558
+ usage.get("cache_creation_1h_input_tokens"),
1559
+ )
1370
1560
  cache_read_cost = _tiered(
1371
1561
  usage.get("cache_read_input_tokens", 0),
1372
1562
  "cache_read_input_token_cost",
@@ -1,9 +1,12 @@
1
1
  """Pure-fn kernel for the pricing-freshness check (spec 2026-05-29).
2
2
 
3
- No I/O, no import of `cctally`/`_lib_pricing` at module scope — every
4
- dependency (pricing predicates, tables, observed rows, LiteLLM snapshot)
5
- is passed in by the I/O glue in bin/cctally. Re-exported there like the
6
- other _lib_* kernels.
3
+ No I/O, and no import of `cctally`/`_lib_pricing` at module scope. The
4
+ tables, observed rows and LiteLLM snapshot are passed in by the I/O glue in
5
+ bin/cctally, which re-exports this module like the other _lib_* kernels.
6
+ The one exception is `check_table_shapes`, which resolves the pure
7
+ `_lib_pricing` kernel per call through `_pricing()` (#929) so the
8
+ whole-request declaration rule has a single definition shared with the
9
+ cost engine's selector.
7
10
  """
8
11
  from __future__ import annotations
9
12
 
@@ -186,6 +189,13 @@ def expired_allowlist_entries(allowlist, as_of_date) -> list:
186
189
  ]
187
190
 
188
191
 
192
+ def _pricing():
193
+ """The pricing kernel, resolved per call (never bound at module scope —
194
+ see this module's docstring). Mirrors ``_lib_cost_provenance._pricing``."""
195
+ import _lib_pricing
196
+ return _lib_pricing
197
+
198
+
189
199
  _CLAUDE_REQUIRED = ("input_cost_per_token", "output_cost_per_token",
190
200
  "cache_creation_input_token_cost", "cache_read_input_token_cost")
191
201
  _CODEX_REQUIRED = ("input_cost_per_token", "cache_read_input_token_cost",
@@ -211,8 +221,14 @@ def check_table_shapes(claude_tbl, codex_tbl, zero_sentinels) -> list:
211
221
  if cost_fields and all(float(v) == 0.0 for v in cost_fields.values()) and not allow_zero:
212
222
  problems.append(f"{model}: all cost fields zero but not a documented sentinel")
213
223
 
224
+ # #929: a Claude whole-request (prompt-length) tier is all-or-nothing.
225
+ # The definition of "well formed" lives beside the selector that refuses a
226
+ # malformed card, so this check and the cost engine can never disagree.
227
+ declaration_problems = _pricing().claude_whole_request_declaration_problems
214
228
  for model, body in claude_tbl.items():
215
229
  _check(model, body, _CLAUDE_REQUIRED, allow_zero=False)
230
+ for p in declaration_problems(body):
231
+ problems.append(f"{model}: {p}")
216
232
  for model, body in codex_tbl.items():
217
233
  _check(model, body, _CODEX_REQUIRED, allow_zero=model in zero_sentinels)
218
234
  return problems
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """The one parser of tests/quarantine.json, the reviewed flaky-test list (spec 4.3).
3
+
4
+ Stdlib only. An entry's `id` takes one of two forms:
5
+
6
+ * a pytest node id, `tests/<file>.py::<node>` (a prefix: pytest's --deselect
7
+ semantics cover every parametrization of a listed function);
8
+ * a harness case, `harness:<harness-name>::<case>`, where `<harness-name>` is
9
+ the `NAME` of `bin/cctally-NAME-test` and `<case>` is one scenario of that
10
+ harness's own case inventory (`bin/cctally-NAME-test --list-cases`).
11
+
12
+ `bin/cctally-test-all`, `bin/_lib_merge_gate.py` and the quick gate read the
13
+ list through this module; the scheduled CI quarantine leg calls its CLI. An
14
+ absent list quarantines nothing (the public mirror may carry none); a list
15
+ that exists but cannot be read raises QuarantineError, never "nothing listed".
16
+
17
+ CLI (exit 0, or 2 on an unreadable list or bad usage):
18
+ python3 bin/_lib_quarantine.py pytest-ids
19
+ python3 bin/_lib_quarantine.py harness-cases <harness>
20
+ """
21
+
22
+ from dataclasses import dataclass
23
+ import json
24
+ from pathlib import Path
25
+ import re
26
+ import subprocess
27
+ import sys
28
+
29
+ LIST_PATH = Path('tests') / 'quarantine.json'
30
+ FIELDS = ('id', 'evidence', 'invariant', 'issue', 'replacement', 'reviewBy')
31
+ PYTEST_ID = re.compile(r'tests/[^:\s]+\.py::\S+')
32
+ HARNESS_ID = re.compile(r'harness:([a-z0-9][a-z0-9-]*)::([A-Za-z0-9][A-Za-z0-9_.-]*)')
33
+
34
+
35
+ class QuarantineError(ValueError):
36
+ """tests/quarantine.json exists but is not a list this parser can apply."""
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Entry:
41
+ id: str
42
+ kind: str # 'pytest' or 'harness'
43
+ harness: str | None # the harness name for a harness case
44
+ case: str | None # the case name for a harness case
45
+ fields: dict
46
+
47
+
48
+ def parse_id(entry_id: str) -> tuple[str, str | None, str | None]:
49
+ """(kind, harness, case) of one id; QuarantineError for any other form."""
50
+ if not isinstance(entry_id, str):
51
+ raise QuarantineError(f'an entry id must be a string, not {entry_id!r}')
52
+ if PYTEST_ID.fullmatch(entry_id):
53
+ return 'pytest', None, None
54
+ match = HARNESS_ID.fullmatch(entry_id)
55
+ if match:
56
+ return 'harness', match.group(1), match.group(2)
57
+ raise QuarantineError(
58
+ f'{entry_id!r} is neither a pytest node id (tests/<file>.py::<node>) '
59
+ 'nor a harness case (harness:<harness>::<case>)')
60
+
61
+
62
+ def parse(doc) -> list[Entry]:
63
+ if not isinstance(doc, list):
64
+ raise QuarantineError('expected a JSON list of entries')
65
+ entries = []
66
+ for item in doc:
67
+ if not isinstance(item, dict):
68
+ raise QuarantineError(f'expected each entry to be an object, not {item!r}')
69
+ kind, harness, case = parse_id(item.get('id'))
70
+ entries.append(Entry(item['id'], kind, harness, case, dict(item)))
71
+ ids = [e.id for e in entries]
72
+ if len(set(ids)) != len(ids):
73
+ raise QuarantineError('duplicate entry ids: ' + ', '.join(
74
+ sorted({i for i in ids if ids.count(i) > 1})))
75
+ return entries
76
+
77
+
78
+ def load_path(path: Path) -> list[Entry]:
79
+ path = Path(path)
80
+ if not path.is_file():
81
+ return []
82
+ try:
83
+ doc = json.loads(path.read_text(encoding='utf-8'))
84
+ except (OSError, ValueError) as exc:
85
+ raise QuarantineError(f'{path}: {exc}') from None
86
+ return parse(doc)
87
+
88
+
89
+ def load(repo: Path) -> list[Entry]:
90
+ return load_path(Path(repo) / LIST_PATH)
91
+
92
+
93
+ def pytest_ids(entries) -> list[str]:
94
+ return [e.id for e in entries if e.kind == 'pytest']
95
+
96
+
97
+ def harness_cases(entries, harness: str) -> list[str]:
98
+ return [e.case for e in entries if e.kind == 'harness' and e.harness == harness]
99
+
100
+
101
+ def harness_inventory(repo: Path, harness: str, *, timeout: float = 60) -> list[str]:
102
+ """The case names `bin/cctally-<harness>-test --list-cases` prints.
103
+
104
+ QuarantineError when the harness does not exist or has no case inventory:
105
+ a harness case that cannot be validated against the harness is not
106
+ quarantinable."""
107
+ script = Path(repo) / 'bin' / f'cctally-{harness}-test'
108
+ if not script.is_file():
109
+ raise QuarantineError(f'harness:{harness}: bin/cctally-{harness}-test does not exist')
110
+ proc = subprocess.run([str(script), '--list-cases'], cwd=repo, capture_output=True,
111
+ text=True, timeout=timeout)
112
+ if proc.returncode != 0:
113
+ raise QuarantineError(f'harness:{harness}: --list-cases exited {proc.returncode}: '
114
+ + (proc.stderr or proc.stdout).strip()[-400:])
115
+ return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
116
+
117
+
118
+ def validate_harness_cases(repo: Path, entries) -> None:
119
+ """Each harness case names an existing harness and a case in its inventory."""
120
+ inventories: dict[str, list[str]] = {}
121
+ for entry in entries:
122
+ if entry.kind != 'harness':
123
+ continue
124
+ if entry.harness not in inventories:
125
+ inventories[entry.harness] = harness_inventory(repo, entry.harness)
126
+ if entry.case not in inventories[entry.harness]:
127
+ raise QuarantineError(f'{entry.id}: no case {entry.case!r} in the '
128
+ f'{entry.harness} harness inventory')
129
+
130
+
131
+ def main(argv=None) -> int:
132
+ argv = list(sys.argv[1:] if argv is None else argv)
133
+ repo = Path(__file__).resolve().parents[1]
134
+ if len(argv) >= 2 and argv[0] == '--repo':
135
+ repo, argv = Path(argv[1]), argv[2:]
136
+ try:
137
+ if argv == ['pytest-ids']:
138
+ out = pytest_ids(load(repo))
139
+ elif len(argv) == 2 and argv[0] == 'harness-cases':
140
+ out = harness_cases(load(repo), argv[1])
141
+ else:
142
+ print('usage: _lib_quarantine.py [--repo DIR] pytest-ids | harness-cases <harness>',
143
+ file=sys.stderr)
144
+ return 2
145
+ except QuarantineError as exc:
146
+ print(f'quarantine: {exc}', file=sys.stderr)
147
+ return 2
148
+ for item in out:
149
+ print(item)
150
+ return 0
151
+
152
+
153
+ if __name__ == '__main__':
154
+ raise SystemExit(main())
@@ -62,6 +62,10 @@ class ApplyRejection(str, enum.Enum):
62
62
 
63
63
  UNSUPPORTED_COMPOSITION = "unsupported-composition"
64
64
  EMPTY_POPULATION = "no-local-history"
65
+ #: The requested interval, or an entry of the population, lies outside
66
+ #: the regime's own coefficient era (#978 spec 4.6). Units from another
67
+ #: era are never divided by this regime's units-per-point.
68
+ UNVALIDATED_COEFFICIENT_ERA = "unvalidated-coefficient-era"
65
69
 
66
70
 
67
71
  @dataclasses.dataclass(frozen=True)
@@ -88,6 +92,9 @@ class ValidatedRegime:
88
92
  class_shares: dict
89
93
  family_radius: "float | None"
90
94
  class_radius: "float | None"
95
+ #: The id of the coefficient era the regime was fitted in, always a
96
+ #: member of `qm.COEFFICIENT_ERAS` (#978).
97
+ coefficient_era: str
91
98
  qualifications: tuple = ()
92
99
 
93
100
 
@@ -196,6 +203,13 @@ def validate_regime(raw, *, account_key, expected_fingerprint,
196
203
  if isinstance(revision, bool) or not isinstance(revision, int) \
197
204
  or revision != expected_revision:
198
205
  return RegimeRejection.REVISION
206
+ # #978 spec 4.5: under the current constants a regime names the
207
+ # coefficient era it was fitted in. Missing or unknown is malformed, never
208
+ # a guess at an era; a regime from an older build has already been
209
+ # reported above as a fingerprint mismatch.
210
+ era = qm.era_by_id(raw.get("coefficientEra"))
211
+ if era is None:
212
+ return RegimeRejection.MALFORMED
199
213
 
200
214
  point = raw.get("unitsPerPoint")
201
215
  if not _finite(point) or point <= 0.0:
@@ -256,31 +270,42 @@ def validate_regime(raw, *, account_key, expected_fingerprint,
256
270
  class_shares=class_shares,
257
271
  family_radius=family_radius,
258
272
  class_radius=class_radius,
273
+ coefficient_era=era.id,
259
274
  qualifications=marks,
260
275
  )
261
276
 
262
277
 
263
- def population_units(entries) -> float:
278
+ def population_units(entries, *, era) -> float:
264
279
  """Summed weighted quota units over the general-meter entries.
265
280
 
266
281
  The same filter `_cctally_quota_model.analyse_account` applies to the
267
282
  current week: an entry whose family does not drain the general weekly
268
283
  meter contributes nothing, and one whose cache-write split is unknown is
269
- skipped rather than priced at zero.
284
+ skipped rather than priced at zero. Every entry is weighted with `era`,
285
+ the regime's own coefficient era (#978).
286
+
287
+ An entry `era` does not cover raises `CoefficientEraMismatch`, as the
288
+ kernel's population functions do (#978 spec 4.2, invariant 5): skipping
289
+ it would divide part of the population by the regime's budget and report
290
+ the result as the whole.
270
291
  """
271
292
  total = 0.0
272
293
  for entry in entries:
273
294
  if qm.family_participation(qm.normalize_family(entry.model)) \
274
295
  != "general":
275
296
  continue
276
- units = qm.weighted_units(entry)
277
- if units is None:
297
+ weighed = qm.weigh_entry(entry, era)
298
+ if weighed.cause == qm.ERA_UNAVAILABLE:
299
+ raise qm.CoefficientEraMismatch(
300
+ f"entry.at {entry.at.isoformat()} lies outside coefficient "
301
+ f"era {era.id!r}")
302
+ if weighed.units is None:
278
303
  continue
279
- total += units
304
+ total += weighed.units
280
305
  return total
281
306
 
282
307
 
283
- def apply_regime(regime: ValidatedRegime, entries
308
+ def apply_regime(regime: ValidatedRegime, entries, *, interval
284
309
  ) -> "AppliedQuota | ApplyRejection":
285
310
  """Modelled consumption for `entries` under `regime`.
286
311
 
@@ -288,8 +313,28 @@ def apply_regime(regime: ValidatedRegime, entries
288
313
  status, because that status describes S1's fit population and this one is
289
314
  the consumer's. The arithmetic afterwards reproduces the kernel's, so a
290
315
  consumer never re-derives the inversion by hand.
316
+
317
+ `interval` is the half-open `[start, end)` the caller models, which a
318
+ consumer extrapolating over a whole week must pass as that week (#978
319
+ spec 4.6). Two independent checks run before any aggregation: the
320
+ interval must lie inside the regime's coefficient era, and so must every
321
+ entry. The interval check is not implied by the entry check — a week
322
+ starting before the era whose retained entries all fall after its start
323
+ is in-era entry by entry, yet modelling the whole week is forbidden.
291
324
  """
292
- aggregated = qm.aggregate_composition(entries)
325
+ era = qm.era_by_id(regime.coefficient_era)
326
+ if era is None:
327
+ return ApplyRejection.UNVALIDATED_COEFFICIENT_ERA
328
+ start, end = interval
329
+ qm.require_aware(start, "interval.start")
330
+ qm.require_aware(end, "interval.end")
331
+ era_start, era_end = qm.era_bounds(era)
332
+ if start < era_start or (era_end is not None and end > era_end):
333
+ return ApplyRejection.UNVALIDATED_COEFFICIENT_ERA
334
+ entries = list(entries)
335
+ if any(not qm.era_contains(era, entry.at) for entry in entries):
336
+ return ApplyRejection.UNVALIDATED_COEFFICIENT_ERA
337
+ aggregated = qm.aggregate_composition(entries, era=era)
293
338
  if aggregated is None:
294
339
  return ApplyRejection.EMPTY_POPULATION
295
340
  family_shares, class_shares = aggregated
@@ -298,7 +343,7 @@ def apply_regime(regime: ValidatedRegime, entries
298
343
  regime.class_shares, regime.class_radius):
299
344
  return ApplyRejection.UNSUPPORTED_COMPOSITION
300
345
 
301
- units = population_units(entries)
346
+ units = population_units(entries, era=era)
302
347
  if not _finite(units) or units < 0.0:
303
348
  return ApplyRejection.EMPTY_POPULATION
304
349
 
@@ -70,7 +70,8 @@ _LONG_FORM: dict = {
70
70
  "the models in use sit outside the mix the calibration was fitted "
71
71
  "over",
72
72
  "unvalidated-coefficient-era":
73
- "this window predates the token weights the model is validated for",
73
+ "this window is not inside a single coefficient era the token "
74
+ "weights are validated for",
74
75
  "stale":
75
76
  "the stored calibration was fitted under different constants, so it "
76
77
  "no longer describes this build",