From 02d0aa5ac4b8cdf017d4729c2522042354793cd3 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 4 Sep 2026 23:48:43 -0400 Subject: [PATCH 1/6] docs(plans): correct a stale protected-file criterion in the quota plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan required the tariff lines in config/config.yaml to stay uncommitted. PR #26 moved deployment values into the gitignored config/config.local.yaml overlay, so config/config.yaml is clean and fully tracked now. Left as written, an executor would hunt for tariff lines that are not there and treat an ordinary tracked file as untouchable -- which matters here because §3 of this plan edits comments in config/config.yaml deliberately. Points the criterion at the overlay instead, which is the file that is not in git history and cannot be restored. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U --- plans/quota-balance-and-burn-rate.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plans/quota-balance-and-burn-rate.md b/plans/quota-balance-and-burn-rate.md index 8b36a8f..cc5ff29 100644 --- a/plans/quota-balance-and-burn-rate.md +++ b/plans/quota-balance-and-burn-rate.md @@ -128,7 +128,15 @@ it appears only in `metrics.py` reporting and the admin allowlist. - The admin quota chip leads with balance and runway. - Existing `/metrics` `quota` keys still present. - Full suite green with `local_energy.enabled` both true and false. -- The user's `config/config.yaml` tariff lines remain uncommitted and verbatim. +- The user's `config/config.local.yaml` is byte-identical after the run + (`local_energy.enabled: true`, `tariff_usd_per_kwh: 0.159`). **Corrected + 2026-09-04:** this criterion previously said the tariff lines in + `config/config.yaml` must stay uncommitted. That is stale — PR #26 moved + deployment values into the gitignored overlay, so `config/config.yaml` is + now clean and tracked. Note the consequence for this plan specifically: + §3 edits comments *in* `config/config.yaml`, which is now an ordinary + tracked edit to commit deliberately, not a file to tiptoe around. The file + to protect is the overlay, and it is not in git history at all. ## The pattern worth naming -- 2.49.1 From b5006f986967556d5744c2031718381839955e44 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 5 Sep 2026 00:28:30 -0400 Subject: [PATCH 2/6] feat(metrics): compute balance, burn rate, and runway from allowance_remaining_usd --- src/metrics.py | 227 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 213 insertions(+), 14 deletions(-) diff --git a/src/metrics.py b/src/metrics.py index f83d112..efdb2df 100644 --- a/src/metrics.py +++ b/src/metrics.py @@ -9,8 +9,10 @@ optionally a ``RouterConfig`` instance; none rely on module-level globals. Functions --------- -quota_burn — kWh metered in the last 30 d, against the plan allowance; - also reports the 30-day reset date +quota_burn — kWh metered in the last 30 d and in the current billing + period, against the plan allowance; also reports the account + credit balance, burn rate and runway from + allowance_remaining_usd scoring_coverage — which scoring axes actually have data recent_decisions — last N rows from the route_decisions observability table per_model — per-model aggregates over energy_observations (last 30 d) @@ -68,6 +70,165 @@ def _next_reset_date(billing_reset_day: int, today: Optional[date] = None) -> st return date(next_year, next_month, billing_reset_day).isoformat() +def _billing_period_start(billing_reset_day: int, today: Optional[date] = None) -> str: + """Return the most recent billing-period start as an ISO date string. + + When *today*'s day-of-month is on or after *billing_reset_day*, returns + this month's reset day; otherwise returns the previous month's reset day + (handling January → December rollover). + """ + if today is None: + today = datetime.now(timezone.utc).date() + if today.day >= billing_reset_day: + return date(today.year, today.month, billing_reset_day).isoformat() + if today.month == 1: + prev_year = today.year - 1 + prev_month = 12 + else: + prev_year = today.year + prev_month = today.month - 1 + return date(prev_year, prev_month, billing_reset_day).isoformat() + + +def quota_balance_and_burn(conn: sqlite3.Connection, cfg: Any) -> dict: + """Latest account balance and its burn/runway from ``allowance_remaining_usd``. + + Pure read of ``energy_observations`` — no dispatcher import. The burn + rate is intentionally conservative: only the most recent + monotonically-decreasing balance segment is used, and two guards stop a + fresh credit top-up from producing a wild extrapolation. + """ + # Read optional knobs from config; treat None as unset and use code defaults. + # Test configs use SimpleNamespace without these attributes, so getattr + # must have a fallback and then a second default when the attr is None. + burn_window_hours = getattr(cfg.objective, "quota_burn_window_hours", None) + if burn_window_hours is None: + burn_window_hours = 24 + + warning_hours = getattr(cfg.objective, "quota_runway_warning_hours", None) + if warning_hours is None: + warning_hours = 6 + + min_samples = getattr(cfg.objective, "quota_burn_min_segment_samples", None) + if min_samples is None: + min_samples = 3 + + min_hours = getattr(cfg.objective, "quota_burn_min_segment_hours", None) + if min_hours is None: + min_hours = 0.5 + + result: dict[str, Any] = { + "balance_usd": None, + "balance_at": None, + "burn_window_hours": burn_window_hours, + "burn_rate_usd_per_hour": None, + "projected_hours_remaining": None, + "runway_low_warning": False, + "runway_note": None, + } + + # 1. Latest non-NULL allowance across ALL rows (no time-window filter). + balance_row = conn.execute( + """ + SELECT observed_at, allowance_remaining_usd + FROM energy_observations + WHERE allowance_remaining_usd IS NOT NULL + ORDER BY observed_at DESC, id DESC + LIMIT 1 + """ + ).fetchone() + if balance_row is not None: + result["balance_usd"] = float(balance_row["allowance_remaining_usd"]) + result["balance_at"] = balance_row["observed_at"] + + # 2. In-window rows for burn estimation. + rows = conn.execute( + """ + SELECT observed_at, allowance_remaining_usd + FROM energy_observations + WHERE allowance_remaining_usd IS NOT NULL + AND julianday(observed_at) > julianday('now', '-' || ? || ' hours') + ORDER BY observed_at ASC, id ASC + """, + (str(burn_window_hours),), + ).fetchall() + + if not rows: + result["runway_note"] = ( + "burn estimate unavailable: no decreasing balance samples in the current window" + ) + return result + + # 3. Split into monotonically-decreasing segments at every balance INCREASE. + # A top-up (credit jump) starts a new segment; only the latest survives. + segments: list[list[tuple[datetime, float]]] = [[]] + for row in rows: + raw_ts = row["observed_at"] + try: + ts = datetime.fromisoformat(raw_ts) + except ValueError: + # Defensive: malformed timestamp would otherwise break /metrics. + continue + value = float(row["allowance_remaining_usd"]) + current = segments[-1] + if not current: + current.append((ts, value)) + elif value > current[-1][1]: + segments.append([(ts, value)]) + else: + current.append((ts, value)) + + latest_segment = segments[-1] + if not latest_segment: + result["runway_note"] = ( + "burn estimate unavailable: no decreasing balance samples in the current window" + ) + return result + + # 4. Guarded burn-rate computation. + if len(latest_segment) < min_samples: + result["runway_note"] = ( + f"burn estimate unavailable: segment after last balance increase has only " + f"{len(latest_segment)} sample(s), need {min_samples}" + ) + return result + + first_ts, first_balance = latest_segment[0] + last_ts, last_balance = latest_segment[-1] + elapsed_hours = (last_ts - first_ts).total_seconds() / 3600.0 + if elapsed_hours < min_hours: + minutes = elapsed_hours * 60 + if minutes < 60: + duration_str = f"{minutes:.0f} minutes" + else: + duration_str = f"{elapsed_hours:.1f} hours" + result["runway_note"] = ( + f"burn estimate unavailable: segment after last balance increase spans only " + f"{duration_str}, need at least {min_hours} h" + ) + return result + + total_decrease = first_balance - last_balance + if total_decrease <= 0.0: + # Treat a flat or increasing-only segment like the no-burn case. + result["runway_note"] = ( + "burn estimate unavailable: no decreasing balance samples in the current window" + ) + return result + + burn_rate = round(total_decrease / elapsed_hours, 6) + result["burn_rate_usd_per_hour"] = burn_rate + + # 5. Projected runway and warning. + balance = result["balance_usd"] + if balance is not None and burn_rate > 0.0: + projected = balance / burn_rate + result["projected_hours_remaining"] = projected + result["runway_low_warning"] = projected < warning_hours + + return result + + def quota_burn( conn: sqlite3.Connection, cfg: Any, @@ -80,13 +241,21 @@ def quota_burn( dispatcher. Returns a dict with ``plan_kwh``, ``metered_kwh_30d``, - ``metered_fraction_of_plan``, ``metered_calls_30d``, ``reset_date`` (the - ISO date of today minus 30 days, the rolling-window start) and ``note``. + ``metered_kwh_period``, ``metered_fraction_of_plan``, + ``metered_calls_30d``, ``reset_date`` (the billing-period start), + ``window_start_30d`` (the rolling 30-day window start), ``note`` and the + balance/burn/runway fields from ``quota_balance_and_burn``. When ``cfg.objective.billing_reset_day`` is set, also returns ``next_reset_date`` — the upcoming billing-period reset day. """ + # This gate removes the report only; it is intentionally not used to refuse + # or alter request dispatch — routing decisions remain independent of quota. if not cfg.objective.plan_kwh_per_period: return None + + plan = cfg.objective.plan_kwh_per_period + window_start_30d = (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat() + row = conn.execute( """ SELECT COALESCE(SUM(energy_kwh), 0) kwh, COUNT(*) n @@ -94,17 +263,43 @@ def quota_burn( WHERE julianday(observed_at) > julianday('now', '-30 days') """ ).fetchone() + metered_kwh_30d = round(float(row["kwh"]), 5) + metered_calls_30d = row["n"] + + reset_day = getattr(cfg.objective, "billing_reset_day", None) + if reset_day is not None: + period_start = _billing_period_start(reset_day) + period_row = conn.execute( + """ + SELECT COALESCE(SUM(energy_kwh), 0) kwh + FROM energy_observations + WHERE julianday(observed_at) >= julianday(?) + """, + (period_start,), + ).fetchone() + metered_kwh_period = round(float(period_row["kwh"]), 5) + metered_fraction_of_plan = round(metered_kwh_period / plan, 4) + reset_date = period_start + else: + metered_kwh_period = None + metered_fraction_of_plan = round(metered_kwh_30d / plan, 4) + reset_date = None - plan = cfg.objective.plan_kwh_per_period result = { "plan_kwh": plan, - "metered_kwh_30d": round(float(row["kwh"]), 5), - "metered_fraction_of_plan": round(float(row["kwh"]) / plan, 4), - "metered_calls_30d": row["n"], - "reset_date": (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat(), + "metered_kwh_30d": metered_kwh_30d, + "metered_kwh_period": metered_kwh_period, + "metered_fraction_of_plan": metered_fraction_of_plan, + "metered_calls_30d": metered_calls_30d, + "reset_date": reset_date, + "window_start_30d": window_start_30d, "note": "router-metered only; traffic bypassing the router is not counted", } - reset_day = getattr(cfg.objective, "billing_reset_day", None) + + # Merge the balance/burn/runway block; quota_balance_and_burn also supplies + # the burn_window_hours default. + result.update(quota_balance_and_burn(conn, cfg)) + if reset_day is not None: result["next_reset_date"] = _next_reset_date(reset_day) return result @@ -152,8 +347,8 @@ def scoring_coverage( if quota and quota["metered_fraction_of_plan"] > 0.8: warnings.append( f"metered usage is {quota['metered_fraction_of_plan']*100:.0f}% of the " - f"{quota['plan_kwh']} kWh plan allowance. A quota is a wall, not a bill — " - "requests fail rather than costing more." + f"{quota['plan_kwh']} kWh plan allowance. Overage is billed against the " + "account's credit balance; plan_kwh_per_period gates nothing." ) if missing_energy: warnings.append( @@ -544,10 +739,12 @@ def local_energy_summary(conn: sqlite3.Connection, cfg) -> Optional[dict]: "metered_cost_usd_30d": round(float(total["cost"]), 5), "calls_30d": total["n"], "by_type": by_type, - "reset_date": (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat(), + "reset_date": None, + "window_start_30d": (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat(), } reset_day = getattr(cfg.objective, "billing_reset_day", None) if reset_day is not None: + result["reset_date"] = _billing_period_start(reset_day) result["next_reset_date"] = _next_reset_date(reset_day) return result @@ -639,10 +836,12 @@ def pinch_summary(conn: sqlite3.Connection, cfg: Any) -> Optional[dict]: "total_tokens_saved": total_tokens_saved, "median_tokens_saved": median_tokens_saved, "dollars_saved_usd_30d": round(dollars_saved_usd_30d, 6), + "reset_date": None, + "window_start_30d": (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat(), } reset_day = getattr(cfg.objective, "billing_reset_day", None) if reset_day is not None: - result["reset_date"] = (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat() + result["reset_date"] = _billing_period_start(reset_day) result["next_reset_date"] = _next_reset_date(reset_day) return result -- 2.49.1 From 7e5737e15cb3fb7c5a253d5e405e62eb2161b80c Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 5 Sep 2026 00:29:05 -0400 Subject: [PATCH 3/6] config: add quota burn/runway knobs and correct quota framing --- CLAUDE.md | 8 ++++++++ config/config.yaml | 33 +++++++++++++++++++++++++-------- src/config.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bfc9930..b428b2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,14 @@ the one case that was checked live it agrees with the measurement in direction and magnitude (7.8x predicted vs 5.0x measured). It is also free, needs no sweep, and refreshes whenever the poller runs. +`objective.plan_kwh_per_period` is a planning figure only: per-request traffic +is **never** refused for exceeding it — it **gates nothing**. Overage is billed +against the account's credit balance (`allowance_remaining_usd` from the +provider). `/metrics` now reports balance, estimated burn rate, and projected +runway (hours remaining) derived from the provider-reported +`allowance_remaining_usd` over a configurable window, using a configurable +minimum segment length and sample count to avoid wild extrapolations. + Three signals said `deepseek-v4-flash` — catalog token price (7.8x cheaper), NeuralWatt's own published per-request energy (~10x lower), and a live 70k measurement (5.0x cheaper). Only the 400-token benchmark disagreed. Trust the diff --git a/config/config.yaml b/config/config.yaml index df5545d..cf8f81d 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -47,19 +47,36 @@ objective: # Per-request ceiling on measured ENERGY, in kWh. null disables it. # - # Denominated in kWh rather than dollars because the plan is a subscription - # with a 6.25 kWh quota, not pay-per-request. Dollars accrue; a quota is a - # wall you hit mid-task. So this is the cost mandate stated as a guarantee. - # For scale: the reference task runs ~5e-06 kWh on the cheapest model and - # ~2.2e-04 on the most expensive. + # Denominated in kWh rather than dollars. For scale: the reference task runs + # ~5e-06 kWh on the cheapest model and ~2.2e-04 on the most expensive. + # Overage is billed against the account's credit balance; + # plan_kwh_per_period gates nothing. max_energy_per_request: # The subscription's kWh allowance per billing period, for reporting burn in - # /health. Set to match your plan; null disables the report. NeuralWatt also - # returns allowance_remaining_usd per request, which is logged, but that is a - # dollar figure while the plan is denominated in energy. + # /health. This is a planning figure only: per-request traffic is never + # refused for exceeding plan_kwh_per_period — it gates nothing. Set to match + # your plan; null disables the report. NeuralWatt also returns + # allowance_remaining_usd per request, which is logged for /metrics, but that + # is a dollar figure while the plan is denominated in energy. plan_kwh_per_period: 6.25 + # Hours of recent balance history used to estimate the burn rate. The most + # recent monotonically-decreasing segment of allowance_remaining_usd values + # (segments split at each balance increase) is examined over this window. + quota_burn_window_hours: 24 + # Projected hours of runway below which /metrics and dashboards emit a + # low-warning boolean; only fires when a burn rate estimate exists and the + # projected remainder is positive but short. + quota_runway_warning_hours: 6 + # A burn estimate needs at least this many balance samples in the most recent + # monotonically-decreasing segment (post-top-up resets the segment); below + # which the estimate is None with an explanatory runway note. + quota_burn_min_segment_samples: 3 + # ...and the segment must span at least this many hours, otherwise the + # estimate is None with an explanatory note (never a wild extrapolation). + quota_burn_min_segment_hours: 0.5 + # The day-of-month your NeuralWatt subscription billing cycle resets. Set # this to YOUR real billing day so the admin quota modal shows a genuine # next-reset date instead of a misleading rolling-window start. null disables diff --git a/src/config.py b/src/config.py index 56588ec..b19c0b5 100644 --- a/src/config.py +++ b/src/config.py @@ -50,6 +50,10 @@ class Objective(StrictModel): max_energy_per_request: Optional[float] = None plan_kwh_per_period: Optional[float] = None billing_reset_day: Optional[int] = None + quota_burn_window_hours: Optional[int] = None + quota_runway_warning_hours: Optional[int] = None + quota_burn_min_segment_samples: Optional[int] = None + quota_burn_min_segment_hours: Optional[float] = None @field_validator("quality_tolerance") @classmethod @@ -75,6 +79,42 @@ class Objective(StrictModel): ) return v + @field_validator("quota_burn_window_hours") + @classmethod + def burn_window_positive(cls, v: Optional[int]) -> Optional[int]: + if v is not None and v <= 0: + raise ValueError( + "objective.quota_burn_window_hours must be > 0" + ) + return v + + @field_validator("quota_runway_warning_hours") + @classmethod + def runway_positive(cls, v: Optional[int]) -> Optional[int]: + if v is not None and v <= 0: + raise ValueError( + "objective.quota_runway_warning_hours must be > 0" + ) + return v + + @field_validator("quota_burn_min_segment_samples") + @classmethod + def segment_samples_positive(cls, v: Optional[int]) -> Optional[int]: + if v is not None and v <= 0: + raise ValueError( + "objective.quota_burn_min_segment_samples must be > 0" + ) + return v + + @field_validator("quota_burn_min_segment_hours") + @classmethod + def segment_hours_positive(cls, v: Optional[float]) -> Optional[float]: + if v is not None and v <= 0: + raise ValueError( + "objective.quota_burn_min_segment_hours must be > 0" + ) + return v + class ContextOverride(StrictModel): """Per-model context handling, for a row whose real limits are known. -- 2.49.1 From ca2e3df169ab3d452eb02790250b2c34fbf4f0f9 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 5 Sep 2026 00:29:05 -0400 Subject: [PATCH 4/6] feat(admin): quota chip and modal lead with balance and runway --- admin/frontend/index.html | 106 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/admin/frontend/index.html b/admin/frontend/index.html index 1326f4e..9a59b6a 100644 --- a/admin/frontend/index.html +++ b/admin/frontend/index.html @@ -460,7 +460,7 @@ header.navbar{padding-top:2px!important;padding-bottom:2px!important}