0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
548 lines
20 KiB
Python
548 lines
20 KiB
Python
"""Data models for Anthropic subscription window tracking.
|
||
|
||
Mirrors the Anthropic OAuth usage API response exactly, including:
|
||
- five_hour / seven_day rolling windows (utilization + reset times)
|
||
- seven_day_opus / seven_day_sonnet per-model 7-day windows
|
||
- extra_usage overage block (credits stored in cents by Anthropic)
|
||
- Headroom contribution: tokens conserved by compression, CLI filtering, cache
|
||
- Window discrepancy detection (surge pricing, cache-miss anomalies)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _utc_now() -> datetime:
|
||
return datetime.now(timezone.utc)
|
||
|
||
|
||
def _to_utc_iso(dt: datetime) -> str:
|
||
return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||
|
||
|
||
def _parse_timestamp(value: Any) -> datetime | None:
|
||
if not isinstance(value, str) or not value:
|
||
return None
|
||
normalized = value.replace("Z", "+00:00")
|
||
try:
|
||
dt = datetime.fromisoformat(normalized)
|
||
except ValueError:
|
||
return None
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=timezone.utc)
|
||
return dt.astimezone(timezone.utc)
|
||
|
||
|
||
def _safe_float(value: Any) -> float | None:
|
||
if value is None:
|
||
return None
|
||
try:
|
||
return float(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def _safe_int(value: Any) -> int | None:
|
||
if value is None:
|
||
return None
|
||
try:
|
||
return int(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Rate-limit window (five_hour / seven_day / seven_day_opus / seven_day_sonnet)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class RateLimitWindow:
|
||
"""A single rolling rate-limit window returned by the Anthropic usage API.
|
||
|
||
``used`` and ``limit`` are in Anthropic's internal token-equivalent units
|
||
(not raw tokens; Anthropic weights tokens differently per model family).
|
||
``utilization_pct`` is the authoritative 0–100 % figure from the API.
|
||
"""
|
||
|
||
used: int = 0
|
||
limit: int = 0
|
||
utilization_pct: float = 0.0
|
||
resets_at: datetime | None = None
|
||
|
||
@classmethod
|
||
def from_api_dict(cls, data: dict[str, Any]) -> RateLimitWindow:
|
||
return cls(
|
||
used=int(data.get("used") or 0),
|
||
limit=int(data.get("limit") or 0),
|
||
utilization_pct=float(data.get("utilization") or 0.0),
|
||
resets_at=_parse_timestamp(data.get("resets_at")),
|
||
)
|
||
|
||
def seconds_to_reset(self, *, now: datetime | None = None) -> float | None:
|
||
if self.resets_at is None:
|
||
return None
|
||
return max((self.resets_at - (now or _utc_now())).total_seconds(), 0.0)
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"used": self.used,
|
||
"limit": self.limit,
|
||
"utilization_pct": round(self.utilization_pct, 2),
|
||
"resets_at": _to_utc_iso(self.resets_at) if self.resets_at else None,
|
||
"seconds_to_reset": self.seconds_to_reset(),
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Display-time synthesis
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def synthesize_window_render(
|
||
window: RateLimitWindow | None,
|
||
*,
|
||
used_since_reset: int | None,
|
||
now: datetime | None = None,
|
||
window_duration: timedelta,
|
||
window_name: str = "window",
|
||
) -> dict[str, Any]:
|
||
"""Render a rate-limit window for the dashboard, synthesizing post-reset.
|
||
|
||
If ``now`` is past ``window.resets_at``, the cached snapshot is stale: we
|
||
return a synthesized dict whose ``used`` is the local transcript-derived
|
||
token count since the reset (capped at ``limit`` so we never display
|
||
>100%; we undercount tokens spent on Claude Code outside this proxy and
|
||
must never report >100%), and whose ``resets_at`` advances by
|
||
``window_duration`` (marked ``estimated``). Otherwise the cached values
|
||
are returned verbatim with ``synthesized=False``.
|
||
|
||
On any unexpected data shape the function logs a warning and falls back
|
||
to the cached values with ``synthesized=False`` and a ``render_warning``
|
||
string — never raises into the caller.
|
||
|
||
Args:
|
||
window: The cached ``RateLimitWindow`` from the most recent poll.
|
||
used_since_reset: Locally-counted token usage strictly after
|
||
``window.resets_at`` (None if we couldn't compute it).
|
||
now: Override the wall clock for testability. Defaults to UTC now.
|
||
window_duration: Length of the rolling window (e.g. 5h or 7d).
|
||
window_name: Label used in structured logs.
|
||
|
||
Returns:
|
||
A dict matching :meth:`RateLimitWindow.to_dict` plus the keys
|
||
``synthesized: bool``, ``resets_at_estimated: bool`` and optional
|
||
``render_warning: str``.
|
||
"""
|
||
if window is None:
|
||
return {
|
||
"used": 0,
|
||
"limit": 0,
|
||
"utilization_pct": 0.0,
|
||
"resets_at": None,
|
||
"seconds_to_reset": None,
|
||
"synthesized": False,
|
||
"resets_at_estimated": False,
|
||
}
|
||
|
||
cached = window.to_dict()
|
||
cached["synthesized"] = False
|
||
cached["resets_at_estimated"] = False
|
||
|
||
if window.resets_at is None:
|
||
return cached
|
||
|
||
current_now = now if now is not None else _utc_now()
|
||
|
||
try:
|
||
if current_now < window.resets_at:
|
||
logger.debug(
|
||
"event=subscription_window_render_cached "
|
||
"window=%s used=%d limit=%d utilization_pct=%.2f",
|
||
window_name,
|
||
window.used,
|
||
window.limit,
|
||
window.utilization_pct,
|
||
)
|
||
return cached
|
||
|
||
# Past the reset boundary — synthesize.
|
||
limit = max(int(window.limit), 0)
|
||
used_local = int(used_since_reset) if used_since_reset is not None else 0
|
||
if used_local < 0:
|
||
used_local = 0
|
||
# Cap at limit: we undercount tokens spent on Claude Code outside this
|
||
# proxy; never report >100%.
|
||
capped_used = min(used_local, limit) if limit > 0 else used_local
|
||
utilization_pct = (capped_used / limit * 100.0) if limit > 0 else 0.0
|
||
|
||
# Walk forward by whole window_durations from the observed reset to
|
||
# land strictly after `current_now` — handles the rare case where the
|
||
# dashboard is loaded long after the reset (e.g. machine was asleep).
|
||
next_reset = window.resets_at
|
||
while next_reset <= current_now:
|
||
next_reset = next_reset + window_duration
|
||
|
||
seconds_to_reset = max((next_reset - current_now).total_seconds(), 0.0)
|
||
|
||
logger.debug(
|
||
"event=subscription_window_synthesized "
|
||
"window=%s used=%d limit=%d utilization_pct=%.2f "
|
||
"resets_at_estimated=%s",
|
||
window_name,
|
||
capped_used,
|
||
limit,
|
||
utilization_pct,
|
||
_to_utc_iso(next_reset),
|
||
)
|
||
|
||
return {
|
||
"used": capped_used,
|
||
"limit": limit,
|
||
"utilization_pct": round(utilization_pct, 2),
|
||
"resets_at": _to_utc_iso(next_reset),
|
||
"seconds_to_reset": seconds_to_reset,
|
||
"synthesized": True,
|
||
"resets_at_estimated": True,
|
||
}
|
||
except Exception as exc:
|
||
# Hard guarantee: never crash the dashboard. Loud warning so the
|
||
# operator can fix the underlying data shape.
|
||
logger.warning(
|
||
"event=subscription_render_synthesis_failed window=%s error=%s",
|
||
window_name,
|
||
exc,
|
||
)
|
||
cached["render_warning"] = f"synthesis_failed: {exc}"
|
||
return cached
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Extra-usage / overage block
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class ExtraUsage:
|
||
"""Overage / extra-usage block from the Anthropic usage API.
|
||
|
||
``monthly_limit_cents`` and ``used_credits_cents`` are in US cents as
|
||
returned by the API (divide by 100 for USD).
|
||
"""
|
||
|
||
is_enabled: bool = False
|
||
monthly_limit_cents: int | None = None
|
||
used_credits_cents: int | None = None
|
||
utilization_pct: float | None = None
|
||
|
||
@classmethod
|
||
def from_api_dict(cls, data: dict[str, Any]) -> ExtraUsage:
|
||
return cls(
|
||
is_enabled=bool(data.get("is_enabled", False)),
|
||
monthly_limit_cents=_safe_int(data.get("monthly_limit")),
|
||
used_credits_cents=_safe_int(data.get("used_credits")),
|
||
utilization_pct=_safe_float(data.get("utilization")),
|
||
)
|
||
|
||
@property
|
||
def monthly_limit_usd(self) -> float | None:
|
||
if self.monthly_limit_cents is None:
|
||
return None
|
||
return self.monthly_limit_cents / 100.0
|
||
|
||
@property
|
||
def used_credits_usd(self) -> float | None:
|
||
if self.used_credits_cents is None:
|
||
return None
|
||
return self.used_credits_cents / 100.0
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"is_enabled": self.is_enabled,
|
||
"monthly_limit_usd": round(self.monthly_limit_usd, 2)
|
||
if self.monthly_limit_usd is not None
|
||
else None,
|
||
"used_credits_usd": round(self.used_credits_usd, 4)
|
||
if self.used_credits_usd is not None
|
||
else None,
|
||
"utilization_pct": round(self.utilization_pct, 2)
|
||
if self.utilization_pct is not None
|
||
else None,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Full snapshot from one API poll
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class SubscriptionSnapshot:
|
||
"""One complete poll of GET /api/oauth/usage."""
|
||
|
||
five_hour: RateLimitWindow = field(default_factory=RateLimitWindow)
|
||
seven_day: RateLimitWindow = field(default_factory=RateLimitWindow)
|
||
seven_day_opus: RateLimitWindow | None = None
|
||
seven_day_sonnet: RateLimitWindow | None = None
|
||
extra_usage: ExtraUsage = field(default_factory=ExtraUsage)
|
||
polled_at: datetime = field(default_factory=_utc_now)
|
||
token_prefix: str = ""
|
||
"""First 8 chars of the OAuth token (for multi-account detection)."""
|
||
|
||
@classmethod
|
||
def from_api_response(cls, data: dict[str, Any], *, token: str = "") -> SubscriptionSnapshot:
|
||
snap = cls(token_prefix=token[:8] if token else "")
|
||
if "five_hour" in data and data["five_hour"]:
|
||
snap.five_hour = RateLimitWindow.from_api_dict(data["five_hour"])
|
||
if "seven_day" in data and data["seven_day"]:
|
||
snap.seven_day = RateLimitWindow.from_api_dict(data["seven_day"])
|
||
if "seven_day_opus" in data and data["seven_day_opus"]:
|
||
snap.seven_day_opus = RateLimitWindow.from_api_dict(data["seven_day_opus"])
|
||
if "seven_day_sonnet" in data and data["seven_day_sonnet"]:
|
||
snap.seven_day_sonnet = RateLimitWindow.from_api_dict(data["seven_day_sonnet"])
|
||
if "extra_usage" in data and data["extra_usage"]:
|
||
snap.extra_usage = ExtraUsage.from_api_dict(data["extra_usage"])
|
||
return snap
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
d: dict[str, Any] = {
|
||
"five_hour": self.five_hour.to_dict(),
|
||
"seven_day": self.seven_day.to_dict(),
|
||
"extra_usage": self.extra_usage.to_dict(),
|
||
"polled_at": _to_utc_iso(self.polled_at),
|
||
"token_prefix": self.token_prefix,
|
||
}
|
||
if self.seven_day_opus:
|
||
d["seven_day_opus"] = self.seven_day_opus.to_dict()
|
||
if self.seven_day_sonnet:
|
||
d["seven_day_sonnet"] = self.seven_day_sonnet.to_dict()
|
||
return d
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Transcript-based window token breakdown
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class WindowTokens:
|
||
"""Token breakdown from Claude transcript JSONL files for one time window."""
|
||
|
||
input: int = 0
|
||
output: int = 0
|
||
cache_reads: int = 0
|
||
cache_writes_5m: int = 0
|
||
cache_writes_1h: int = 0
|
||
cache_writes_total: int = 0
|
||
by_model: dict[str, dict[str, int]] = field(default_factory=dict)
|
||
weighted_token_equivalent: float = 0.0
|
||
"""Sonnet-normalised weighted total (opus×2, sonnet×1, haiku×0.5)."""
|
||
|
||
def total_raw(self) -> int:
|
||
return self.input + self.output + self.cache_reads + self.cache_writes_total
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"input": self.input,
|
||
"output": self.output,
|
||
"cache_reads": self.cache_reads,
|
||
"cache_writes_5m": self.cache_writes_5m,
|
||
"cache_writes_1h": self.cache_writes_1h,
|
||
"cache_writes_total": self.cache_writes_total,
|
||
"total_raw": self.total_raw(),
|
||
"weighted_token_equivalent": round(self.weighted_token_equivalent, 1),
|
||
"by_model": self.by_model,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Headroom contribution estimate
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class HeadroomContribution:
|
||
"""Tokens conserved within the current 5h window by Headroom's layers.
|
||
|
||
These are cumulative counters reset when the 5h window rolls over.
|
||
"""
|
||
|
||
tokens_submitted: int = 0
|
||
"""Raw input tokens actually forwarded to Anthropic by the proxy."""
|
||
|
||
tokens_saved_compression: int = 0
|
||
"""Input tokens removed by proxy compression."""
|
||
|
||
tokens_saved_cli_filtering: int = 0
|
||
"""Tokens avoided by the selected CLI context tool before reaching context."""
|
||
|
||
tokens_saved_rtk: int = 0
|
||
"""Deprecated alias for CLI filtering tokens from older persisted state."""
|
||
|
||
tokens_saved_cache_reads: int = 0
|
||
"""Input tokens served from Anthropic prefix-cache (discounted reads)."""
|
||
|
||
compression_savings_usd: float = 0.0
|
||
cache_savings_usd: float = 0.0
|
||
|
||
def cli_filtering_saved(self) -> int:
|
||
return max(self.tokens_saved_cli_filtering, self.tokens_saved_rtk)
|
||
|
||
def total_saved(self) -> int:
|
||
return (
|
||
self.tokens_saved_compression
|
||
+ self.cli_filtering_saved()
|
||
+ self.tokens_saved_cache_reads
|
||
)
|
||
|
||
def compression_saved(self) -> int:
|
||
"""Tokens removed before model context by compression plus CLI filtering."""
|
||
|
||
return self.tokens_saved_compression + self.cli_filtering_saved()
|
||
|
||
def total_savings_usd(self) -> float:
|
||
return self.compression_savings_usd + self.cache_savings_usd
|
||
|
||
def raw_without_headroom(self) -> int:
|
||
return self.tokens_submitted + self.tokens_saved_compression + self.cli_filtering_saved()
|
||
|
||
def efficiency_pct(self) -> float:
|
||
raw = self.raw_without_headroom()
|
||
if raw == 0:
|
||
return 0.0
|
||
return round(self.total_saved() / raw * 100, 1)
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"tokens_submitted": self.tokens_submitted,
|
||
"tokens_saved": {
|
||
"compression": self.compression_saved(),
|
||
"proxy_compression": self.tokens_saved_compression,
|
||
"cli_filtering": self.cli_filtering_saved(),
|
||
"rtk": self.cli_filtering_saved(),
|
||
# PR-G2 (Realignment) — raw counters, distinct from the
|
||
# dashboard-facing ``cli_filtering`` / ``rtk`` keys (which
|
||
# both report ``max(cli_filtering, rtk)`` for legacy
|
||
# display). Persisted so the tracker can round-trip each
|
||
# counter independently — the bug PR-G2 retires is that
|
||
# ``tokens_saved_rtk`` and ``tokens_saved_cli_filtering``
|
||
# used to be identical.
|
||
"cli_filtering_raw": self.tokens_saved_cli_filtering,
|
||
"rtk_raw": self.tokens_saved_rtk,
|
||
"cache_reads": self.tokens_saved_cache_reads,
|
||
"total": self.total_saved(),
|
||
},
|
||
"raw_without_headroom": self.raw_without_headroom(),
|
||
"efficiency_pct": self.efficiency_pct(),
|
||
"savings_usd": {
|
||
"compression": round(self.compression_savings_usd, 4),
|
||
"cache": round(self.cache_savings_usd, 4),
|
||
"total": round(self.total_savings_usd(), 4),
|
||
},
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Anomaly / discrepancy record
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class WindowDiscrepancy:
|
||
"""Detected anomaly between expected and API-reported utilization."""
|
||
|
||
kind: str
|
||
"""'surge_pricing' | 'cache_miss' | 'none'"""
|
||
|
||
description: str = ""
|
||
severity: str = "info"
|
||
"""'info' | 'warning' | 'alert'"""
|
||
|
||
expected_utilization_pct: float | None = None
|
||
actual_utilization_pct: float | None = None
|
||
delta_pct: float | None = None
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"kind": self.kind,
|
||
"description": self.description,
|
||
"severity": self.severity,
|
||
"expected_utilization_pct": self.expected_utilization_pct,
|
||
"actual_utilization_pct": self.actual_utilization_pct,
|
||
"delta_pct": self.delta_pct,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Full tracker state
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass
|
||
class SubscriptionState:
|
||
"""Persistent state for the subscription tracker."""
|
||
|
||
latest: SubscriptionSnapshot | None = None
|
||
window_tokens: WindowTokens | None = None
|
||
"""Transcript-derived token breakdown for the current 5h window."""
|
||
|
||
contribution: HeadroomContribution = field(default_factory=HeadroomContribution)
|
||
discrepancies: list[WindowDiscrepancy] = field(default_factory=list)
|
||
history: list[SubscriptionSnapshot] = field(default_factory=list)
|
||
|
||
poll_count: int = 0
|
||
poll_errors: int = 0
|
||
last_error: str | None = None
|
||
last_active_at: datetime | None = None
|
||
|
||
_MAX_HISTORY: int = field(default=100, init=False, repr=False)
|
||
_MAX_DISCREPANCIES: int = field(default=20, init=False, repr=False)
|
||
|
||
def add_snapshot(self, snapshot: SubscriptionSnapshot) -> None:
|
||
self.latest = snapshot
|
||
self.history.append(snapshot)
|
||
if len(self.history) > self._MAX_HISTORY:
|
||
self.history = self.history[-self._MAX_HISTORY :]
|
||
self.poll_count += 1
|
||
|
||
def mark_error(self, msg: str) -> None:
|
||
self.poll_errors += 1
|
||
self.last_error = msg
|
||
|
||
def add_discrepancy(self, d: WindowDiscrepancy) -> None:
|
||
self.discrepancies.append(d)
|
||
if len(self.discrepancies) > self._MAX_DISCREPANCIES:
|
||
self.discrepancies = self.discrepancies[-self._MAX_DISCREPANCIES :]
|
||
|
||
def is_active(self, *, active_window_s: float = 60.0) -> bool:
|
||
if self.last_active_at is None:
|
||
return False
|
||
return (_utc_now() - self.last_active_at).total_seconds() <= active_window_s
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"latest": self.latest.to_dict() if self.latest else None,
|
||
"window_tokens": self.window_tokens.to_dict() if self.window_tokens else None,
|
||
"contribution": self.contribution.to_dict(),
|
||
"discrepancies": [d.to_dict() for d in self.discrepancies[-5:]],
|
||
"poll_count": self.poll_count,
|
||
"poll_errors": self.poll_errors,
|
||
"last_error": self.last_error,
|
||
"last_active_at": _to_utc_iso(self.last_active_at) if self.last_active_at else None,
|
||
}
|
||
|
||
def to_persist_dict(self) -> dict[str, Any]:
|
||
d = self.to_dict()
|
||
d["history"] = [s.to_dict() for s in self.history[-20:]]
|
||
return d
|