feat: add generalised tier system with BYOK and local vision unlocks
This commit is contained in:
parent
76506a390e
commit
97ee2c20b6
3 changed files with 125 additions and 0 deletions
3
circuitforge_core/tiers/__init__.py
Normal file
3
circuitforge_core/tiers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .tiers import can_use, tier_label, TIERS, BYOK_UNLOCKABLE, LOCAL_VISION_UNLOCKABLE
|
||||
|
||||
__all__ = ["can_use", "tier_label", "TIERS", "BYOK_UNLOCKABLE", "LOCAL_VISION_UNLOCKABLE"]
|
||||
76
circuitforge_core/tiers/tiers.py
Normal file
76
circuitforge_core/tiers/tiers.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
Tier system for CircuitForge products.
|
||||
|
||||
Tiers: free < paid < premium < ultra
|
||||
Products register their own FEATURES dict and pass it to can_use().
|
||||
|
||||
BYOK_UNLOCKABLE: features that unlock when the user has any configured
|
||||
LLM backend (local or API key). These are gated only because CF would
|
||||
otherwise provide the compute.
|
||||
|
||||
LOCAL_VISION_UNLOCKABLE: features that unlock when the user has a local
|
||||
vision model configured (e.g. moondream2). Distinct from BYOK — a text
|
||||
LLM key does NOT unlock vision features.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
TIERS: list[str] = ["free", "paid", "premium", "ultra"]
|
||||
|
||||
# Features that unlock when the user has any LLM backend configured.
|
||||
# Each product extends this frozenset with its own BYOK-unlockable features.
|
||||
BYOK_UNLOCKABLE: frozenset[str] = frozenset()
|
||||
|
||||
# Features that unlock when the user has a local vision model configured.
|
||||
LOCAL_VISION_UNLOCKABLE: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
def can_use(
|
||||
feature: str,
|
||||
tier: str,
|
||||
has_byok: bool = False,
|
||||
has_local_vision: bool = False,
|
||||
_features: dict[str, str] | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Return True if the given tier (and optional unlocks) can access feature.
|
||||
|
||||
Args:
|
||||
feature: Feature key string.
|
||||
tier: User's current tier ("free", "paid", "premium", "ultra").
|
||||
has_byok: True if user has a configured LLM backend.
|
||||
has_local_vision: True if user has a local vision model configured.
|
||||
_features: Feature→min_tier map. Products pass their own dict here.
|
||||
If None, all features are free.
|
||||
"""
|
||||
features = _features or {}
|
||||
if feature not in features:
|
||||
return True
|
||||
|
||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
||||
return True
|
||||
|
||||
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
|
||||
return True
|
||||
|
||||
min_tier = features[feature]
|
||||
try:
|
||||
return TIERS.index(tier) >= TIERS.index(min_tier)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def tier_label(
|
||||
feature: str,
|
||||
has_byok: bool = False,
|
||||
has_local_vision: bool = False,
|
||||
_features: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
"""Return a human-readable label for the minimum tier needed for feature."""
|
||||
features = _features or {}
|
||||
if feature not in features:
|
||||
return "free"
|
||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
||||
return "free (BYOK)"
|
||||
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
|
||||
return "free (local vision)"
|
||||
return features[feature]
|
||||
46
tests/test_tiers.py
Normal file
46
tests/test_tiers.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import pytest
|
||||
from circuitforge_core.tiers import can_use, TIERS, BYOK_UNLOCKABLE, LOCAL_VISION_UNLOCKABLE
|
||||
|
||||
|
||||
def test_tiers_order():
|
||||
assert TIERS == ["free", "paid", "premium", "ultra"]
|
||||
|
||||
|
||||
def test_free_feature_always_accessible():
|
||||
# Features not in FEATURES dict are free for everyone
|
||||
assert can_use("nonexistent_feature", tier="free") is True
|
||||
|
||||
|
||||
def test_paid_feature_blocked_for_free_tier():
|
||||
# Caller must register features — test via can_use with explicit min_tier
|
||||
assert can_use("test_paid", tier="free", _features={"test_paid": "paid"}) is False
|
||||
|
||||
|
||||
def test_paid_feature_accessible_for_paid_tier():
|
||||
assert can_use("test_paid", tier="paid", _features={"test_paid": "paid"}) is True
|
||||
|
||||
|
||||
def test_premium_feature_accessible_for_ultra_tier():
|
||||
assert can_use("test_premium", tier="ultra", _features={"test_premium": "premium"}) is True
|
||||
|
||||
|
||||
def test_byok_unlocks_byok_feature():
|
||||
byok_feature = next(iter(BYOK_UNLOCKABLE)) if BYOK_UNLOCKABLE else None
|
||||
if byok_feature:
|
||||
assert can_use(byok_feature, tier="free", has_byok=True) is True
|
||||
|
||||
|
||||
def test_byok_does_not_unlock_non_byok_feature():
|
||||
assert can_use("test_paid", tier="free", has_byok=True,
|
||||
_features={"test_paid": "paid"}) is False
|
||||
|
||||
|
||||
def test_local_vision_unlocks_vision_feature():
|
||||
vision_feature = next(iter(LOCAL_VISION_UNLOCKABLE)) if LOCAL_VISION_UNLOCKABLE else None
|
||||
if vision_feature:
|
||||
assert can_use(vision_feature, tier="free", has_local_vision=True) is True
|
||||
|
||||
|
||||
def test_local_vision_does_not_unlock_non_vision_feature():
|
||||
assert can_use("test_paid", tier="free", has_local_vision=True,
|
||||
_features={"test_paid": "paid"}) is False
|
||||
Loading…
Reference in a new issue