Skip to content

fix(subscriptions): read the utilization header as a fraction, past 1.0 too (#2419) - #2864

Merged
vybe merged 2 commits into
devfrom
feature/2419-utilization-fraction
Sep 17, 2026
Merged

vybe merged 2 commits into
devfrom
feature/2419-utilization-fraction

Conversation

@webmixgamer

Copy link
Copy Markdown
Contributor

Description

_parse_utilization converted the provider's anthropic-ratelimit-unified-{5h,7d}-utilization header with round(v * 100, 1) if v <= 1.0 else round(v, 1). The header is a fraction of the cap, so an overage reading of 1.2 (120%) came out as 1.2%: Settings and the dashboard tile showed a nearly-empty subscription, the ent#434 weekly alert classified it has_headroom and never fired, and the #2409 auto-switch ranker rated the exhausted subscription the best destination in the fleet.

  • The parser now always multiplies by 100, matching the provider's own client (Claude Code reads the header as Number(value) behind Number.isFinite, no <= 1 branch, no clamp on 5h/7d, displays Math.round(u * 100)).
  • Non-finite or negative results are None. The guard sits on the scaled value: a finite 1e307 overflows to inf only after the multiply, and round(inf, 1) returns inf rather than raising. -0.0 is normalised so it never renders as -0%.
  • One INFO line in _probe logs the raw header strings when a window reads past 100% — the only path by which the issue's "capture a real overage header set" criterion can ever close. Standing query: SELECT * FROM subscription_headroom_history WHERE five_hour_utilization_pct > 100 OR seven_day_utilization_pct > 100.
  • Two stale comments (db_models.py # 0..100, the alert threshold ceiling note) and the module docstring updated; the feature flow gains two clauses; one learnings-ledger entry.
  • No frontend change: subscriptionPressureTile.js::barWidthPct already clamps the bar and reports the unclamped number (spec has a 137% case); the Settings panel prints the raw value. No migration: both DB tracks store the figure as REAL/Float with no bound; no pydantic le=100.

Evidence, stated honestly. "The header is a fraction" is proven (the 2026-08-19 spike headers, 1,333 local probe rows, the provider client's parser). "It exceeds 1.0 on an overage plan" is open in both directions: no capture exists, every local plan carries overage_status = rejected and blocks at 100%, and the public docs do not document the unified family. The fix is right regardless — Trinity now agrees with the provider's client for every input; a percent-shaped "42.5" reads loud-wrong (4250%) on both, versus today's quiet-plausible 1.2%. The overage header set in the tests is synthetic, modelled on that parser, and says so.

Plan-gate rulings (2026-09-17): AC #4 ("existing #471 tests unchanged") is amended for exactly one assertion — _parse_utilization("42.5") == 42.5 pinned the defect and is replaced; every other #471 / ent#433 / ent#434 / #2409 test is untouched. Negative input → None is a stated behaviour change (a -0.1 reading classified has_headroom before, unassessable now; the ranker's own guard stays because pre-fix Redis snapshots live up to 7 days). A follow-up issue for overage-aware predicates (overage_status / unified_status are parsed and persisted but read by no predicate) is filed separately.

Related Issue

Fixes #2419

Journey Impact

Journey Impact: none: a parser unit fix inside the headroom probe — no journey promise is added or extended; the subscription journeys read the percentage the probe already produced

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Testing

  • I have tested this locally — end to end on a live instance, table below
  • New tests added — tests/unit/test_2419_utilization_fraction.py (parser table, synthetic overage header set, the filed bug through the REAL chain: headers → _to_modelclassify_headroom == saturated + selection_verdict == (measured, 120.0), decide_tier CRIT, ranker sorts the overage subscription last, _history_row passthrough, the arrival log in both directions)
  • All existing tests pass — 530 across every subscription suite (test_471, test_ent434, test_2409, test_447, test_433, test_2396, test_2638, test_2443, test_2572, test_ent582, test_1677, test_297, test_subscription_auto_switch_pingpong); the full unit island was still running at push time and its tally follows as a PR comment
  • Every new test executes the changed path — every assertion calls the parser, the header reader, the model builder, the classifier, the ranker or the probe; none reads source text

Mutation: tests/unit/test_2419_utilization_fraction.py (20 cases across TestParser, TestHeaders, TestChain, TestArrivalSignal) and tests/unit/test_471_subscription_usage_observability.py::TestHeadroomParsing::test_header_is_a_fraction_never_a_percent go RED without the fix — observed on the pre-fix tree before the change was applied: 21 failed / 37 passed on the two files, green after (58/58).

Local end-to-end (live instance, dev backend hot-reloaded from the bind mount)

Check Result
Real probe through the new code provider 0.11 / 0.36 → 11% / 36%, history row written, no arrival line
Synthetic 120% snapshot stored via the service's own _store_snapshot GET /usage → 120.0 both windows; GET /api/agents/subscription-pressure → the agent on it reads 120.0
Settings → Subscriptions render 5h: 120% · 7d: 120%
Classifier / ranker over the live cache saturated; the overage subscription ranked last of three
ent#434 weekly alert on the next sweep fired: "Subscription '…' is at 120% of its weekly limit", priority high
Restore real refresh probe overwrote the snapshot; test alert cancelled; 0 history rows past 100

Checklist

  • My code follows the project's style guidelines
  • I have updated the documentation (if applicable)
  • I have not committed any sensitive data (API keys, credentials, etc.)
  • I have added appropriate logging for new functionality

Residuals (also in the commit message)

Cached Redis snapshots self-heal within one refresh interval (≤1 h unwatched, ≤15 min watched, 60 s after a click). Pre-fix history rows cannot be repaired — 1.2 from "0.012" and from "1.2" are byte-identical. The first post-deploy sweep may raise critical operator-queue items for every overage subscription, bounded by the per-window dedup id and the per-cycle cap.

Security: /cso --diff — 0 findings, report in docs/security-reports/cso-diff-2026-09-17-2419-utilization-fraction.md.

🤖 Generated with Claude Code

….0 too (#2419)

The provider's anthropic-ratelimit-unified-{5h,7d}-utilization header is a
fraction of the cap. _parse_utilization multiplied by 100 only up to 1.0 and
passed anything larger through as an "already-percent" value, so an overage
reading of 1.2 came out as 1.2%: the ent#434 weekly alert filed an exhausted
subscription as has_headroom and the #2409 ranker rated it the best
destination. The parser now always scales, matching the provider's own
client; non-finite or negative results are None (the guard sits on the scaled
value, since a finite 1e307 overflows only after the multiply), and -0.0 is
normalised. A window reading past 100% logs the raw header strings at INFO,
the capture path for the first real overage sample.

Residuals: cached Redis snapshots self-heal within one refresh interval;
pre-fix history rows cannot be repaired (1.2 from "0.012" and from "1.2" are
identical); the first post-deploy sweep may raise critical items for every
overage subscription; a negative reading now classifies as unassessable
rather than has_headroom.

Fixes #2419

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…llow-up, #2865

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@webmixgamer

Copy link
Copy Markdown
Contributor Author

Follow-up for the part deliberately left out of this PR — the status predicates never read overage_status / unified_status, so a subscription serving on overage may be mis-badged, and the first real overage header set still needs capturing — is #2865. The security report's appendix item A1 now cites it (second commit on this branch, docs only).

@webmixgamer

Copy link
Copy Markdown
Contributor Author

Full unit island on this branch (cd tests && pytest unit -q -p no:randomly, Python 3.12, 11 min): 18 failed, 16,490 passed, 34 skipped. Zero failures in anything this PR touches.

  • 13 × unit/test_ent615_token_free_remotes.py — the red dev this branch was cut from (the refactor: split the four remaining oversized backend modules (#1028) #2487 split; M1 in the release work order). In isolation: 89 passed, 16 warnings in 10.14s.
  • 5 × unit/test_2703_skill_assign_delivery.py — order-dependent in a full sequential run on this clone (a polluter sorts before it); pre-existing on origin/dev. In isolation: 25 passed, 16 warnings in 0.54s.

The 14 subscription suites this change reaches (test_471, test_ent434, test_2409, test_447, test_433, test_2396, test_2638, test_2443, test_2572, test_ent582, test_1677, test_297, test_subscription_auto_switch_pingpong, and the new test_2419) are 530/530 green.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20260917-1237 (#2868)

@vybe
vybe merged commit 9dcd77a into dev Sep 17, 2026
36 of 37 checks passed
@webmixgamer
webmixgamer deleted the feature/2419-utilization-fraction branch September 17, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants