Files
ai-agent/backend/schemas/usage.py
Gabriel Vidal 4bedccc005 feat(usage): record the official /usage % and calibrate plan units per 1 %
backend/usage/ polls each account's rate-limits.json (read-only mount,
USAGE_STATE_DIRS) and keeps every change (+ a 30 min heartbeat) in
usage_limits. A daily fit over 14 days of hourly rises vs the runs'
meterBins gives plan units per 1 % of the 5h / week window; only trusted
fits (n>=24, R2>=0.5, within x3 of the seed) replace the rate, and a >20 %
shift pushes a notification. Cards carry planPct, --final notifications
get ' · $X · ≈Y% wk', and /api/usage/{limits,history,calibration,
calibrate,import} serve the Plan usage page (owner only).
scripts/usage-backfill-ha.py imports the HA hourly statistics since July.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:13:55 +02:00

112 lines
2.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Plan usage: the official /usage percentages (rate-limits.json snapshots, HA
backfill) and their calibration against the plan units conversations spend."""
from __future__ import annotations
from typing import Literal, Optional
from .base import Schema
__all__ = [
"LimitWindow",
"WindowFit",
"Calibration",
"AccountLimits",
"UsageLimitsResponse",
"LimitPoint",
"HourMeter",
"DayMeter",
"UsageHistory",
"CalibrationHistory",
"CalibrateResult",
"UsageImportResult",
]
class LimitWindow(Schema):
pct: float
resetsAt: Optional[float] = None
class WindowFit(Schema):
"""One window's exchange rate: plan units (and list $) per 1 %."""
unitsPerPct: float
usdPerPct: Optional[float] = None
r2: Optional[float] = None
n: int = 0
interceptPct: Optional[float] = None
class Calibration(Schema):
account: str
fittedAt: float
days: int
source: Literal["fit", "default"]
# Passed the bar to become the rate in force (enough hours, R², and within
# ×3 of the seed rate); untrusted fits are kept for the history chart.
trusted: bool = False
week: WindowFit
h5: WindowFit
class AccountLimits(Schema):
account: str
label: Optional[str] = None
# "five_hour", "seven_day" (and any model-specific "seven_day_<model>").
windows: dict[str, LimitWindow] = {}
updatedAt: Optional[float] = None
stale: bool = True
calibration: Optional[Calibration] = None
class UsageLimitsResponse(Schema):
accounts: dict[str, AccountLimits]
class LimitPoint(Schema):
t: float
pct: float
resetsAt: Optional[float] = None
class HourMeter(Schema):
t: float
cost: float
units: float
dWeek: Optional[float] = None
d5h: Optional[float] = None
class DayMeter(Schema):
day: str
cost: float
units: float
observedWeek: Optional[float] = None
predictedWeek: Optional[float] = None
untrackedWeek: Optional[float] = None
observed5h: Optional[float] = None
predicted5h: Optional[float] = None
class UsageHistory(Schema):
account: str
days: int
series: dict[str, list[LimitPoint]]
hourly: list[HourMeter]
daily: list[DayMeter]
calibration: Optional[Calibration] = None
class CalibrationHistory(Schema):
account: str
current: Optional[Calibration] = None
history: list[Calibration] = []
class CalibrateResult(Schema):
calibrations: dict[str, Calibration]
shifted: list[str] = []
class UsageImportResult(Schema):
imported: int