feat(entitlements): min_tier_for_all + extend /required-tier-batch to capacity axes#3230
Merged
Merged
Conversation
…r-batch to capacity axes
The /api/entitlement/required-tier-batch endpoint took features=<csv> and
runtimes=<csv> only -- a dashboard mixing axes ("you're using fleet +
claude_code + 5 channels + 30-day retention + 2 nodes -- what tier?")
needed N round-trips and a max-by-rank on the client.
* clawmetry/entitlements.py: new module-level min_tier_for_all(*, features,
runtimes, channels, retention_days, nodes) that folds the five
per-axis min_tier_for_* helpers + max-by-rank into one canonical
reverse lookup. retention_days=None means "axis not supplied" (NOT
unlimited -> Enterprise, which would mis-route every call that omits
retention); the per-axis posture matches the plural helpers' "unknown
contributes nothing, never raise" contract.
* routes/entitlement.py: /api/entitlement/required-tier-batch now also
accepts channels=<int>, retention_days=<int>, nodes=<int>. Blank /
non-int values are treated as "not supplied" (matches the singular
endpoint's never-crash posture). The 400 "supply at least one axis"
guard now covers all five axes.
* tests/test_entitlements_min_tier_batch.py: 17 new tests pinning
min_tier_for_all semantics + the extended endpoint contract (capacity-
only calls satisfy the "at least one axis" rule, blank/non-int
capacity is silently skipped, mixed-axis calls take the most-
constraining tier across all five).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Visual diffComparing No flagged differences (running with ALWAYS_POST=1).
Folder: pr/3230/005eaa52d6d6. Full PNGs also attached as a workflow artefact. Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes a gap in
/api/entitlement/required-tier-batch: it only acceptedfeatures=<csv>andruntimes=<csv>, even though the singular/api/entitlement/required-tiersupports all five axes (feature, runtime, channels, retention_days, nodes). A dashboard mixing axes ("you're using fleet + claude_code + 5 channels + 30-day retention + 2 nodes — what tier covers this?") needed N round-trips and a max-by-rank on the client.Changes
clawmetry/entitlements.py— new module-level helpermin_tier_for_all(*, features, runtimes, channels, retention_days, nodes). Folds the five per-axismin_tier_for_*helpers + max-by-rank into one canonical reverse lookup.retention_days=Nonemeans axis not supplied (NOT unlimited → Enterprise, which would mis-route every call that omits retention). Never raises.routes/entitlement.py—/api/entitlement/required-tier-batchnow also acceptschannels=<int>,retention_days=<int>,nodes=<int>. Blank / non-int values are silently treated as "not supplied" (matches the singular endpoint's never-crash posture rather than mis-routing a typo to Enterprise). The 400 "supply at least one axis" guard now covers all five axes. Existingfeatures=/runtimes=contract unchanged.tests/test_entitlements_min_tier_batch.py— 17 new tests pinningmin_tier_for_allsemantics (no-args → None, single-axis matches singular helper, most-constraining axis wins, capacityNone≠ unlimited, unknown items skipped) + the extended endpoint contract (capacity-only calls satisfy "at least one axis", blank/non-int capacity is silently skipped, mixed five-axis calls resolve in one round-trip).Posture
Entitlementdefaults change; resolved entitlement is stillgrace=True, allallows_*checks still returnTrue. This PR only adds introspection surface; no enforcement gate flips. Safe to ship.features,runtimes,required_tier,required_tier_rank,current_tier,current_tier_rank,upgrade_required,allowed) keep their meaning; three new keys (channels,retention_days,nodes) are added alongside.Test plan
python -m py_compileclean on changed filespytest tests/test_entitlements_min_tier_batch.py— 41 passed (24 pre-existing + 17 new)pytest tests/test_entitlement*.py tests/test_entitlements*.py— 641 passedLocal operator verification checklist
This runs in a remote sandbox with no access to the user's machine, the live daemon,
~/.openclaw, or the cloud snapshot. Verifying live requires the local box:git fetch origin feat/entitlement-min-tier-for-all && git checkout feat/entitlement-min-tier-for-all.__pycache__under bothclawmetry/androutes/.launchctl kickstart -k gui/$(id -u)/com.clawmetry.sync.required_tier: "cloud_starter"(most-constraining axis),allowed: true(grace mode), and the newchannels/retention_days/nodeskeys echoed back as ints.required_tier: "enterprise"(no behaviour change vs main).🤖 Generated with Claude Code
Generated by Claude Code