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.
Files changed (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +12 -5
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +912 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +116 -8
  11. package/bin/_cctally_dashboard_share.py +50 -19
  12. package/bin/_cctally_dashboard_sources.py +223 -48
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +350 -44
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +81 -33
  38. package/bin/_lib_dashboard_sources.py +75 -0
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_rederive.py +395 -0
  45. package/bin/_lib_share.py +58 -2
  46. package/bin/cctally +56 -8
  47. package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
  48. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  49. package/dashboard/static/dashboard.html +2 -2
  50. package/package.json +5 -1
  51. 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(model, inp, out, cc, cr, cost_usd_raw) -> float:
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
- usage = {
554
- "input_tokens": inp or 0,
555
- "output_tokens": out or 0,
556
- "cache_creation_input_tokens": cc or 0,
557
- "cache_read_input_tokens": cr or 0,
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
- write = _calculate_entry_cost(model or "", {"cache_creation_input_tokens": lost})
586
- read = _calculate_entry_cost(model or "", {"cache_read_input_tokens": lost})
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(model or "", {"input_tokens": cache_read})
598
- read = _calculate_entry_cost(model or "", {"cache_read_input_tokens": cache_read})
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__(self, *, compaction=False, key=None, cc=0, cr=0, model=None):
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 position
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(model, lost),
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(sql, params):
907
- costs[(m, r)] = _entry_cost(model, inp, out, cc, cr, raw)
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): {"input","output","cache_creation","cache_read"}} for
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
- it["tokens"] = tok
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(it.get("model"), cr_tokens)
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
 
@@ -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
- "input_tokens": e.input_tokens,
517
- "output_tokens": e.output_tokens,
518
- "cache_creation_input_tokens": e.cache_creation_tokens,
519
- "cache_read_input_tokens": e.cache_read_tokens,
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
- {"input_tokens": e.input_tokens, "output_tokens": e.output_tokens,
556
- "cache_creation_input_tokens": e.cache_creation_tokens,
557
- "cache_read_input_tokens": e.cache_read_tokens},
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
- {"input_tokens": e.input_tokens, "output_tokens": e.output_tokens,
597
- "cache_creation_input_tokens": e.cache_creation_tokens,
598
- "cache_read_input_tokens": e.cache_read_tokens},
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
- {"input_tokens": e.input_tokens, "output_tokens": e.output_tokens,
643
- "cache_creation_input_tokens": e.cache_creation_tokens,
644
- "cache_read_input_tokens": e.cache_read_tokens},
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