cctally 1.72.0 → 1.74.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 +22 -0
- package/bin/_cctally_cache.py +271 -55
- package/bin/_cctally_core.py +20 -40
- package/bin/_cctally_dashboard.py +35 -1
- package/bin/_cctally_dashboard_conversation.py +731 -94
- package/bin/_cctally_dashboard_share.py +38 -3
- package/bin/_cctally_dashboard_sources.py +812 -74
- package/bin/_cctally_db.py +104 -1
- package/bin/_cctally_doctor.py +86 -4
- package/bin/_cctally_parser.py +17 -4
- package/bin/_cctally_record.py +162 -33
- package/bin/_cctally_refresh.py +58 -36
- package/bin/_cctally_source_analytics.py +288 -3
- package/bin/_cctally_statusline.py +811 -82
- package/bin/_cctally_transcript.py +223 -7
- package/bin/_cctally_tui.py +17 -14
- package/bin/_lib_aggregators.py +48 -34
- package/bin/_lib_codex_conversation_export.py +124 -0
- package/bin/_lib_codex_conversation_query.py +500 -28
- package/bin/_lib_codex_conversation_watch.py +323 -0
- package/bin/_lib_conversation_dispatch.py +269 -7
- package/bin/_lib_conversation_query.py +88 -0
- package/bin/_lib_dashboard_sources.py +20 -7
- package/bin/_lib_doctor.py +106 -0
- package/bin/_lib_jsonl.py +11 -0
- package/bin/_lib_pricing.py +7 -4
- package/bin/_lib_pricing_check.py +7 -1
- package/bin/_lib_quota.py +12 -11
- package/bin/_lib_statusline_candidates.py +734 -0
- package/bin/_lib_view_models.py +50 -14
- package/bin/cctally +31 -2
- package/dashboard/static/assets/index-DeQjEMO6.js +80 -0
- package/dashboard/static/assets/index-ZiPO8veo.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BWadAHxC.js +0 -80
- package/dashboard/static/assets/index-D2nwo_ln.css +0 -1
|
@@ -2540,6 +2540,94 @@ def build_anon_plan_for_db(conn, *, home_dir):
|
|
|
2540
2540
|
home_dirs=sorted(home_dirs), usernames=sorted(usernames))
|
|
2541
2541
|
|
|
2542
2542
|
|
|
2543
|
+
def build_anon_plan_for_sources(conn, *, home_dir, sources):
|
|
2544
|
+
"""Provider-aware :class:`AnonPlan` for QUALIFIED conversation surfaces
|
|
2545
|
+
(#294 S7, §3.6). A SEPARATE builder from :func:`build_anon_plan_for_db`, which
|
|
2546
|
+
stays byte-untouched together with every bare-Claude call site — unioning Codex
|
|
2547
|
+
roots into the legacy builder would renumber its ``project-N`` tokens and change
|
|
2548
|
+
bare-Claude anonymized bytes. This builder is used ONLY by ``v1.``-qualified
|
|
2549
|
+
requests (both providers), which are new and carry no byte-compat debt.
|
|
2550
|
+
|
|
2551
|
+
``sources`` selects the provider legs to include (e.g. ``{"codex"}`` or
|
|
2552
|
+
``{"claude"}``). The Codex leg draws ONLY from the authoritative tables:
|
|
2553
|
+
provider root dirs from ``codex_source_roots.canonical_root_path``, observed
|
|
2554
|
+
project paths from validated ``codex_conversation_threads.cwd`` (absolute paths
|
|
2555
|
+
only), display labels from ``codex_conversation_rollups.project_label``.
|
|
2556
|
+
``git_json`` is NOT a path source (its fields are branch/repository metadata,
|
|
2557
|
+
not filesystem roots). The existing home-dir/username machinery and
|
|
2558
|
+
``SECRET_PATTERNS`` apply unchanged; coverage stays best-effort-over-known-tokens
|
|
2559
|
+
and the per-token replacement stays fail-closed."""
|
|
2560
|
+
from _lib_conversation_anon import build_anon_plan
|
|
2561
|
+
want = set(sources or ())
|
|
2562
|
+
project_roots: dict = {}
|
|
2563
|
+
paths: set = set()
|
|
2564
|
+
if "claude" in want:
|
|
2565
|
+
cwds: set = set()
|
|
2566
|
+
try:
|
|
2567
|
+
for (cwd,) in conn.execute(
|
|
2568
|
+
"SELECT DISTINCT cwd FROM conversation_messages "
|
|
2569
|
+
"WHERE cwd IS NOT NULL AND cwd != ''"):
|
|
2570
|
+
if cwd:
|
|
2571
|
+
cwds.add(cwd)
|
|
2572
|
+
except sqlite3.OperationalError:
|
|
2573
|
+
pass
|
|
2574
|
+
try:
|
|
2575
|
+
for (pp,) in conn.execute(
|
|
2576
|
+
"SELECT DISTINCT project_path FROM session_files "
|
|
2577
|
+
"WHERE project_path IS NOT NULL AND project_path != ''"):
|
|
2578
|
+
if pp:
|
|
2579
|
+
cwds.add(pp)
|
|
2580
|
+
except sqlite3.OperationalError:
|
|
2581
|
+
pass
|
|
2582
|
+
for c in cwds:
|
|
2583
|
+
project_roots[c] = _project_label(c)
|
|
2584
|
+
paths.add(c)
|
|
2585
|
+
if "codex" in want:
|
|
2586
|
+
labels: dict = {}
|
|
2587
|
+
try:
|
|
2588
|
+
for ck, pl in conn.execute(
|
|
2589
|
+
"SELECT conversation_key, project_label FROM codex_conversation_rollups "
|
|
2590
|
+
"WHERE project_label IS NOT NULL AND project_label != ''"):
|
|
2591
|
+
labels[ck] = pl
|
|
2592
|
+
except sqlite3.OperationalError:
|
|
2593
|
+
pass
|
|
2594
|
+
try:
|
|
2595
|
+
for ck, cwd in conn.execute(
|
|
2596
|
+
"SELECT conversation_key, cwd FROM codex_conversation_threads "
|
|
2597
|
+
"WHERE cwd IS NOT NULL AND cwd != ''"):
|
|
2598
|
+
if isinstance(cwd, str) and cwd and os.path.isabs(cwd):
|
|
2599
|
+
project_roots[cwd] = labels.get(ck) or _project_label(cwd)
|
|
2600
|
+
paths.add(cwd)
|
|
2601
|
+
except sqlite3.OperationalError:
|
|
2602
|
+
pass
|
|
2603
|
+
try:
|
|
2604
|
+
for (root_path,) in conn.execute(
|
|
2605
|
+
"SELECT canonical_root_path FROM codex_source_roots "
|
|
2606
|
+
"WHERE canonical_root_path IS NOT NULL AND canonical_root_path != ''"):
|
|
2607
|
+
if isinstance(root_path, str) and root_path and os.path.isabs(root_path):
|
|
2608
|
+
project_roots.setdefault(root_path, _project_label(root_path))
|
|
2609
|
+
paths.add(root_path)
|
|
2610
|
+
except sqlite3.OperationalError:
|
|
2611
|
+
pass
|
|
2612
|
+
home_dirs: set = set()
|
|
2613
|
+
usernames: set = set()
|
|
2614
|
+
if home_dir:
|
|
2615
|
+
home_dirs.add(home_dir)
|
|
2616
|
+
u = os.path.basename(home_dir.rstrip("/"))
|
|
2617
|
+
if u:
|
|
2618
|
+
usernames.add(u)
|
|
2619
|
+
for p in paths:
|
|
2620
|
+
for prefix in ("/Users/", "/home/"):
|
|
2621
|
+
if p.startswith(prefix):
|
|
2622
|
+
user = p[len(prefix):].split("/", 1)[0]
|
|
2623
|
+
if user:
|
|
2624
|
+
home_dirs.add(prefix + user)
|
|
2625
|
+
usernames.add(user)
|
|
2626
|
+
return build_anon_plan(
|
|
2627
|
+
project_roots=project_roots,
|
|
2628
|
+
home_dirs=sorted(home_dirs), usernames=sorted(usernames))
|
|
2629
|
+
|
|
2630
|
+
|
|
2543
2631
|
_TASK_TRIO = ("TaskCreate", "TaskUpdate", "TaskList")
|
|
2544
2632
|
|
|
2545
2633
|
|
|
@@ -272,8 +272,9 @@ def unavailable_source_state(
|
|
|
272
272
|
|
|
273
273
|
def _coherent_provider(state: SourceDashboardState) -> bool:
|
|
274
274
|
return (
|
|
275
|
-
state.availability in ("ok", "empty")
|
|
275
|
+
state.availability in ("ok", "empty", "partial")
|
|
276
276
|
and state.freshness == "fresh"
|
|
277
|
+
and bool(state.data_version)
|
|
277
278
|
and state.data is not None
|
|
278
279
|
)
|
|
279
280
|
|
|
@@ -302,6 +303,10 @@ def _combined_metrics(
|
|
|
302
303
|
) -> Mapping[str, object] | None:
|
|
303
304
|
if not (_coherent_provider(claude) and _coherent_provider(codex)):
|
|
304
305
|
return None
|
|
306
|
+
for state in (claude, codex):
|
|
307
|
+
hero_capability = state.capabilities.get("hero")
|
|
308
|
+
if hero_capability is None or hero_capability.status not in {"supported", "derived"}:
|
|
309
|
+
return None
|
|
305
310
|
try:
|
|
306
311
|
claude_hero = claude.data["hero"]
|
|
307
312
|
codex_hero = codex.data["hero"]
|
|
@@ -360,19 +365,27 @@ def compose_all_state(
|
|
|
360
365
|
if claude.source != "claude" or codex.source != "codex":
|
|
361
366
|
raise ValueError("all composition requires Claude and Codex provider states")
|
|
362
367
|
combined = _combined_metrics(claude, codex)
|
|
363
|
-
|
|
364
|
-
if
|
|
368
|
+
providers_coherent = _coherent_provider(claude) and _coherent_provider(codex)
|
|
369
|
+
if providers_coherent:
|
|
365
370
|
availability: Availability = (
|
|
366
|
-
"
|
|
367
|
-
if
|
|
368
|
-
else
|
|
371
|
+
"partial"
|
|
372
|
+
if combined is None or "partial" in (claude.availability, codex.availability)
|
|
373
|
+
else (
|
|
374
|
+
"empty"
|
|
375
|
+
if claude.availability == "empty" and codex.availability == "empty"
|
|
376
|
+
else "ok"
|
|
377
|
+
)
|
|
369
378
|
)
|
|
370
379
|
freshness: Freshness = "fresh"
|
|
371
380
|
else:
|
|
372
381
|
availability = "partial"
|
|
373
382
|
freshness = "stale"
|
|
374
383
|
version_material = json.dumps(
|
|
375
|
-
[
|
|
384
|
+
[
|
|
385
|
+
claude.data_version, claude.availability, claude.freshness,
|
|
386
|
+
codex.data_version, codex.availability, codex.freshness,
|
|
387
|
+
combined is not None,
|
|
388
|
+
],
|
|
376
389
|
separators=(",", ":"),
|
|
377
390
|
).encode("utf-8")
|
|
378
391
|
data_version = "all:" + hashlib.sha256(version_material).hexdigest()[:24]
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -15,6 +15,7 @@ import dataclasses
|
|
|
15
15
|
import datetime as dt
|
|
16
16
|
import hashlib
|
|
17
17
|
import json
|
|
18
|
+
import math
|
|
18
19
|
import pathlib
|
|
19
20
|
import sys
|
|
20
21
|
from typing import Optional
|
|
@@ -213,6 +214,16 @@ class DoctorState:
|
|
|
213
214
|
# never-WARN posture (the hooks.installed / settings warnings already
|
|
214
215
|
# surface a genuinely-unreadable settings.json — no double-WARN here).
|
|
215
216
|
statusline_refresh_state: str = "unavailable"
|
|
217
|
+
# #318: read-only evidence for the per-session statusline candidate
|
|
218
|
+
# arbitration pipeline. None means the gather could not inspect it;
|
|
219
|
+
# absent transport/tombstones are normal when Claude is closed.
|
|
220
|
+
statusline_pipeline: Optional[dict] = None
|
|
221
|
+
# #312: all-history, root-qualified Codex accounting metadata partition.
|
|
222
|
+
# The I/O layer reports an explicit error instead of silently converting a
|
|
223
|
+
# failed health query into zero rows, so doctor can distinguish an empty
|
|
224
|
+
# retained corpus from unreadable metadata.
|
|
225
|
+
codex_project_metadata_health: Optional[dict] = None
|
|
226
|
+
codex_project_metadata_error: Optional[str] = None
|
|
216
227
|
|
|
217
228
|
|
|
218
229
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -737,6 +748,62 @@ def _check_data_latest_snapshot_age(s: DoctorState) -> CheckResult:
|
|
|
737
748
|
)
|
|
738
749
|
|
|
739
750
|
|
|
751
|
+
def _check_statusline_pipeline(s: DoctorState) -> CheckResult:
|
|
752
|
+
"""Report transport, selected, control, and authority health separately."""
|
|
753
|
+
pipeline = s.statusline_pipeline if isinstance(s.statusline_pipeline, dict) else {}
|
|
754
|
+
tombstones = pipeline.get("tombstones")
|
|
755
|
+
tombstones = tombstones if isinstance(tombstones, dict) else {}
|
|
756
|
+
details = {
|
|
757
|
+
"transport_age_seconds": pipeline.get("transport_age_seconds"),
|
|
758
|
+
"selected_age_seconds": pipeline.get("selected_age_seconds"),
|
|
759
|
+
"active_candidate_count": pipeline.get("active_candidate_count", 0),
|
|
760
|
+
"control_db_agrees": pipeline.get("control_db_agrees"),
|
|
761
|
+
"tombstones": tombstones,
|
|
762
|
+
}
|
|
763
|
+
if any(value in {"invalid", "inflight"} for value in tombstones.values()):
|
|
764
|
+
return CheckResult(
|
|
765
|
+
id="data.statusline_pipeline", title="Statusline pipeline",
|
|
766
|
+
severity="warn", summary="authoritative state needs repair",
|
|
767
|
+
remediation="Run `cctally refresh-usage`", details=details,
|
|
768
|
+
)
|
|
769
|
+
if pipeline.get("control_db_agrees") is False:
|
|
770
|
+
return CheckResult(
|
|
771
|
+
id="data.statusline_pipeline", title="Statusline pipeline",
|
|
772
|
+
severity="warn", summary="selected control disagrees with database",
|
|
773
|
+
remediation="Run `cctally refresh-usage`, then inspect active sessions",
|
|
774
|
+
details=details,
|
|
775
|
+
)
|
|
776
|
+
transport_age = pipeline.get("transport_age_seconds")
|
|
777
|
+
selected_age = pipeline.get("selected_age_seconds")
|
|
778
|
+
transport_age = (
|
|
779
|
+
float(transport_age)
|
|
780
|
+
if isinstance(transport_age, (int, float)) and not isinstance(transport_age, bool)
|
|
781
|
+
else math.inf
|
|
782
|
+
)
|
|
783
|
+
selected_age = (
|
|
784
|
+
float(selected_age)
|
|
785
|
+
if isinstance(selected_age, (int, float)) and not isinstance(selected_age, bool)
|
|
786
|
+
else math.inf
|
|
787
|
+
)
|
|
788
|
+
if transport_age < 90 and selected_age >= 300:
|
|
789
|
+
return CheckResult(
|
|
790
|
+
id="data.statusline_pipeline", title="Statusline pipeline",
|
|
791
|
+
severity="warn", summary="timer active; selected usage stale",
|
|
792
|
+
remediation="Run `cctally refresh-usage`, then inspect active sessions",
|
|
793
|
+
details=details,
|
|
794
|
+
)
|
|
795
|
+
if transport_age >= 90:
|
|
796
|
+
summary = "no recent regular-pool timer observed"
|
|
797
|
+
elif selected_age < 300:
|
|
798
|
+
summary = "timer and selected usage fresh"
|
|
799
|
+
else:
|
|
800
|
+
summary = "selected usage awaiting next active timer"
|
|
801
|
+
return CheckResult(
|
|
802
|
+
id="data.statusline_pipeline", title="Statusline pipeline",
|
|
803
|
+
severity="ok", summary=summary, remediation=None, details=details,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
740
807
|
def _check_data_cache_sync_state(s: DoctorState) -> CheckResult:
|
|
741
808
|
count = s.cache_entries_count or 0
|
|
742
809
|
if count == 0:
|
|
@@ -815,6 +882,43 @@ def _check_data_codex_cache(s: DoctorState) -> CheckResult:
|
|
|
815
882
|
)
|
|
816
883
|
|
|
817
884
|
|
|
885
|
+
def _check_data_codex_project_metadata(s: DoctorState) -> CheckResult:
|
|
886
|
+
"""Report the identity-safe all-history Codex metadata partition."""
|
|
887
|
+
if s.codex_project_metadata_error is not None:
|
|
888
|
+
return CheckResult(
|
|
889
|
+
id="data.codex_project_metadata", title="Codex project metadata",
|
|
890
|
+
severity="fail", summary="metadata health could not be read",
|
|
891
|
+
remediation="Run `cctally doctor --verbose` after checking cache.db.",
|
|
892
|
+
details={"error": s.codex_project_metadata_error},
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
health = s.codex_project_metadata_health or {}
|
|
896
|
+
total_rows = int(health.get("total_rows", 0))
|
|
897
|
+
qualified_rows = int(health.get("qualified_rows", 0))
|
|
898
|
+
missing_key_rows = int(health.get("missing_conversation_key_rows", 0))
|
|
899
|
+
missing_join_rows = int(health.get("missing_thread_join_rows", 0))
|
|
900
|
+
incomplete_rows = missing_key_rows + missing_join_rows
|
|
901
|
+
details = {
|
|
902
|
+
"total_rows": total_rows,
|
|
903
|
+
"qualified_rows": qualified_rows,
|
|
904
|
+
"missing_conversation_key_rows": missing_key_rows,
|
|
905
|
+
"missing_thread_join_rows": missing_join_rows,
|
|
906
|
+
"incomplete_rows": incomplete_rows,
|
|
907
|
+
}
|
|
908
|
+
if incomplete_rows:
|
|
909
|
+
return CheckResult(
|
|
910
|
+
id="data.codex_project_metadata", title="Codex project metadata",
|
|
911
|
+
severity="warn",
|
|
912
|
+
summary=f"{incomplete_rows} accounting row(s) need metadata repair",
|
|
913
|
+
remediation="Run `cctally cache-sync --source codex --rebuild`.",
|
|
914
|
+
details=details,
|
|
915
|
+
)
|
|
916
|
+
return CheckResult(
|
|
917
|
+
id="data.codex_project_metadata", title="Codex project metadata",
|
|
918
|
+
severity="ok", summary="qualified", remediation=None, details=details,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
|
|
818
922
|
def _check_data_codex_quota(s: DoctorState) -> CheckResult:
|
|
819
923
|
"""Report local physical Codex quota evidence without fabricating a window."""
|
|
820
924
|
raw_windows = s.codex_quota_windows or []
|
|
@@ -1658,8 +1762,10 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
1658
1762
|
)),
|
|
1659
1763
|
("data", "Data", (
|
|
1660
1764
|
("data.latest_snapshot_age", "_check_data_latest_snapshot_age"),
|
|
1765
|
+
("data.statusline_pipeline", "_check_statusline_pipeline"),
|
|
1661
1766
|
("data.cache_sync_state", "_check_data_cache_sync_state"),
|
|
1662
1767
|
("data.codex_cache", "_check_data_codex_cache"),
|
|
1768
|
+
("data.codex_project_metadata", "_check_data_codex_project_metadata"),
|
|
1663
1769
|
("data.codex_quota", "_check_data_codex_quota"),
|
|
1664
1770
|
("data.parse_health", "_check_data_parse_health"),
|
|
1665
1771
|
("data.forked_buckets", "_check_data_forked_buckets"),
|
package/bin/_lib_jsonl.py
CHANGED
|
@@ -720,6 +720,14 @@ def _accounting_from_record(
|
|
|
720
720
|
_codex_skip(state, "no-last-token-usage")
|
|
721
721
|
return None, last_total_tokens, filename_session_id_warned
|
|
722
722
|
|
|
723
|
+
# Forked/subagent rollouts can begin with a copied slice of their parent's
|
|
724
|
+
# physical history before the child's first model-bearing turn_context.
|
|
725
|
+
# Those records remain in the physical event stream, but projecting them
|
|
726
|
+
# into accounting would duplicate the parent's usage under model="unknown".
|
|
727
|
+
if (state.model is None and state.thread is not None
|
|
728
|
+
and state.thread.parent_thread_id is not None):
|
|
729
|
+
return None, last_total_tokens, filename_session_id_warned
|
|
730
|
+
|
|
723
731
|
total_token_usage = info.get("total_token_usage")
|
|
724
732
|
if isinstance(total_token_usage, dict):
|
|
725
733
|
try:
|
|
@@ -839,6 +847,9 @@ def _iter_codex_fused_records_with_offsets(
|
|
|
839
847
|
session_id = _codex_string(payload.get("id"))
|
|
840
848
|
if session_id is not None:
|
|
841
849
|
state.session_id = session_id
|
|
850
|
+
session_model = _codex_string(payload.get("model"))
|
|
851
|
+
if session_model is not None:
|
|
852
|
+
state.model = session_model.strip()
|
|
842
853
|
thread_update = _thread_metadata_from_session_meta(
|
|
843
854
|
payload, path_str, source_root_key
|
|
844
855
|
)
|
package/bin/_lib_pricing.py
CHANGED
|
@@ -53,7 +53,7 @@ def _chip_for_model(name: str) -> str:
|
|
|
53
53
|
# Date the embedded pricing snapshots below were last verified against
|
|
54
54
|
# vendor sources. Bump whenever CLAUDE_MODEL_PRICING / CODEX_MODEL_PRICING
|
|
55
55
|
# is synced. Read by `pricing-check` + the release pre-flight staleness nudge.
|
|
56
|
-
PRICING_SNAPSHOT_DATE = "2026-07-
|
|
56
|
+
PRICING_SNAPSHOT_DATE = "2026-07-19"
|
|
57
57
|
PRICING_STALENESS_DAYS = 60 # release pre-flight WARNs past this age
|
|
58
58
|
|
|
59
59
|
# Canonical machine-readable pricing source (Claude values + Codex values).
|
|
@@ -99,7 +99,7 @@ PRICING_DRIFT_ALLOWLIST: list[dict] = [
|
|
|
99
99
|
|
|
100
100
|
# Anthropic API pricing snapshot:
|
|
101
101
|
# - Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
|
102
|
-
# - Captured: 2026-07-
|
|
102
|
+
# - Captured: 2026-07-19 (see PRICING_SNAPSHOT_DATE)
|
|
103
103
|
# - Verified by maintainer against docs.claude.com/en/docs/about-claude/pricing;
|
|
104
104
|
# update in PRs touching this table.
|
|
105
105
|
# 2026-06-10: added claude-fable-5 ($10/$50 per MTok; 1M context, no
|
|
@@ -321,8 +321,8 @@ _unknown_model_warnings: set[str] = set()
|
|
|
321
321
|
#
|
|
322
322
|
# Codex (OpenAI) API pricing snapshot:
|
|
323
323
|
# - Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
|
324
|
-
# - Captured: 2026-
|
|
325
|
-
# - As of the 2026-
|
|
324
|
+
# - Captured: 2026-07-19 (see PRICING_SNAPSHOT_DATE)
|
|
325
|
+
# - As of the 2026-07-19 sync this carries every openai-provider
|
|
326
326
|
# gpt-5* model the LiteLLM snapshot lists, so `pricing-check`'s scope finds
|
|
327
327
|
# nothing missing. Models absent from this table still fall back to `gpt-5`
|
|
328
328
|
# pricing with isFallback=true (matches upstream's LEGACY_FALLBACK_MODEL
|
|
@@ -332,6 +332,9 @@ _unknown_model_warnings: set[str] = set()
|
|
|
332
332
|
# `pricing-check` as missing_from_us (gpt-5.6-sol had real usage falling back
|
|
333
333
|
# to gpt-5). Exact model_prices_and_context_window.json values, each with the
|
|
334
334
|
# above-272k tier (max_input_tokens 1050000).
|
|
335
|
+
# 2026-07-19: verified all tracked values and the complete openai-provider
|
|
336
|
+
# gpt-5* model set against the live LiteLLM snapshot; no named table entries
|
|
337
|
+
# were missing. Official OpenAI model pages were used as the vendor cross-check.
|
|
335
338
|
#
|
|
336
339
|
# Billing rules:
|
|
337
340
|
# - reasoning_output_tokens is billed at the *output* rate (matches
|
|
@@ -34,11 +34,17 @@ class DriftResult:
|
|
|
34
34
|
ahead_of_litellm: list[str] # informational; never actionable
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
_CODEX_UNATTRIBUTED_MODEL = "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
37
40
|
def classify_coverage(observed, resolve_claude, is_codex_fallback) -> list[CoverageGap]:
|
|
38
41
|
"""observed: iterable of (provider, model, entry_count, token_total).
|
|
39
42
|
|
|
40
43
|
Claude model with resolve_claude(model) is None -> kind="unpriced".
|
|
41
44
|
Codex model with is_codex_fallback(model) True -> kind="fallback".
|
|
45
|
+
The ingest-only Codex ``unknown`` sentinel is skipped because it records
|
|
46
|
+
missing model metadata, not a model identifier that can be added to the
|
|
47
|
+
embedded pricing table.
|
|
42
48
|
Priced models produce no gap. Order preserved.
|
|
43
49
|
"""
|
|
44
50
|
gaps: list[CoverageGap] = []
|
|
@@ -46,7 +52,7 @@ def classify_coverage(observed, resolve_claude, is_codex_fallback) -> list[Cover
|
|
|
46
52
|
if provider == "claude":
|
|
47
53
|
if resolve_claude(model) is None:
|
|
48
54
|
gaps.append(CoverageGap("claude", model, "unpriced", entry_count, token_total))
|
|
49
|
-
elif provider == "codex":
|
|
55
|
+
elif provider == "codex" and model != _CODEX_UNATTRIBUTED_MODEL:
|
|
50
56
|
if is_codex_fallback(model):
|
|
51
57
|
gaps.append(CoverageGap("codex", model, "fallback", entry_count, token_total))
|
|
52
58
|
return gaps
|
package/bin/_lib_quota.py
CHANGED
|
@@ -407,7 +407,7 @@ def _single_identity_history(observations: Iterable[QuotaObservation]) -> QuotaH
|
|
|
407
407
|
def forecast_quota(
|
|
408
408
|
observations: Iterable[QuotaObservation], as_of: dt.datetime,
|
|
409
409
|
) -> QuotaForecast:
|
|
410
|
-
"""
|
|
410
|
+
"""Forecast reset-scoped quota use from elapsed native-cycle pace."""
|
|
411
411
|
_require_aware(as_of, "as_of")
|
|
412
412
|
history = _single_identity_history(observations)
|
|
413
413
|
if history is None:
|
|
@@ -423,28 +423,29 @@ def forecast_quota(
|
|
|
423
423
|
if observation.resets_at == baseline.resets_at and observation.captured_at <= as_of
|
|
424
424
|
)
|
|
425
425
|
points = tuple(sorted(points, key=physical_order_key))
|
|
426
|
-
usable_elapsed_seconds = 0.0
|
|
427
|
-
positive_delta_percent = 0.0
|
|
428
426
|
sample_count = 0
|
|
429
427
|
for prior, current in zip(points, points[1:]):
|
|
430
428
|
elapsed_seconds = (current.captured_at - prior.captured_at).total_seconds()
|
|
431
429
|
delta_percent = current.used_percent - prior.used_percent
|
|
432
430
|
if elapsed_seconds > 0 and delta_percent > 0:
|
|
433
|
-
usable_elapsed_seconds += elapsed_seconds
|
|
434
|
-
positive_delta_percent += delta_percent
|
|
435
431
|
sample_count += 1
|
|
436
432
|
|
|
437
433
|
remaining_seconds = max(0, int((baseline.resets_at - as_of).total_seconds()))
|
|
438
|
-
|
|
439
|
-
|
|
434
|
+
native_window_seconds = baseline.identity.window_minutes * 60
|
|
435
|
+
cycle_start = baseline.resets_at - dt.timedelta(minutes=baseline.identity.window_minutes)
|
|
436
|
+
cycle_elapsed_seconds = min(
|
|
437
|
+
float(native_window_seconds),
|
|
438
|
+
max(0.0, (as_of - cycle_start).total_seconds()),
|
|
439
|
+
)
|
|
440
|
+
if sample_count > 0 and cycle_elapsed_seconds > 0:
|
|
441
|
+
rate = baseline.used_percent / (cycle_elapsed_seconds / 3600)
|
|
440
442
|
projected = min(100.0, max(
|
|
441
443
|
baseline.used_percent,
|
|
442
444
|
baseline.used_percent + rate * (remaining_seconds / 3600),
|
|
443
445
|
))
|
|
444
|
-
|
|
445
|
-
if sample_count >= 4 and usable_elapsed_seconds >= native_window_seconds * 0.25:
|
|
446
|
+
if sample_count >= 4 and cycle_elapsed_seconds >= native_window_seconds * 0.25:
|
|
446
447
|
confidence: Literal["high", "medium", "low"] = "high"
|
|
447
|
-
elif sample_count >= 2 and
|
|
448
|
+
elif sample_count >= 2 and cycle_elapsed_seconds >= native_window_seconds * 0.10:
|
|
448
449
|
confidence = "medium"
|
|
449
450
|
else:
|
|
450
451
|
confidence = "low"
|
|
@@ -469,7 +470,7 @@ def forecast_quota(
|
|
|
469
470
|
resets_at=baseline.resets_at,
|
|
470
471
|
remaining_seconds=remaining_seconds,
|
|
471
472
|
sample_count=sample_count,
|
|
472
|
-
sample_span_seconds=int(
|
|
473
|
+
sample_span_seconds=int(cycle_elapsed_seconds) if sample_count > 0 else 0,
|
|
473
474
|
confidence=confidence,
|
|
474
475
|
)
|
|
475
476
|
|