kiwi/app/tiers.py

75 lines
2.3 KiB
Python

"""
Kiwi tier gates.
Tiers: free < paid < premium
(Ultra not used in Kiwi — no human-in-the-loop operations.)
Uses circuitforge-core can_use() with Kiwi's feature map.
"""
from __future__ import annotations
from circuitforge_core.tiers.tiers import can_use as _can_use, BYOK_UNLOCKABLE
# Features that unlock when the user supplies their own LLM backend.
KIWI_BYOK_UNLOCKABLE: frozenset[str] = frozenset({
"recipe_suggestions",
"expiry_llm_matching",
"receipt_ocr",
})
# Feature → minimum tier required
KIWI_FEATURES: dict[str, str] = {
# Free tier
"inventory_crud": "free",
"barcode_scan": "free",
"receipt_upload": "free",
"expiry_alerts": "free",
"export_csv": "free",
"leftover_mode": "free", # Rate-limited at API layer, not tier-gated
"staple_library": "free",
# Paid tier
"receipt_ocr": "paid", # BYOK-unlockable
"recipe_suggestions": "paid", # BYOK-unlockable
"expiry_llm_matching": "paid", # BYOK-unlockable
"meal_planning": "paid",
"dietary_profiles": "paid",
"style_picker": "paid",
# Premium tier
"multi_household": "premium",
"background_monitoring": "premium",
}
def can_use(feature: str, tier: str, has_byok: bool = False) -> bool:
"""Return True if the given tier can access the feature.
The 'local' tier is assigned to dev-bypass and non-cloud sessions —
it has unrestricted access to all features.
"""
if tier == "local":
return True
return _can_use(
feature,
tier,
has_byok=has_byok,
_features=KIWI_FEATURES,
_byok_unlockable=KIWI_BYOK_UNLOCKABLE,
)
def require_feature(feature: str, tier: str, has_byok: bool = False) -> None:
"""Raise ValueError if the tier cannot access the feature."""
if not can_use(feature, tier, has_byok):
from circuitforge_core.tiers.tiers import tier_label
needed = tier_label(
feature,
has_byok=has_byok,
_features=KIWI_FEATURES,
_byok_unlockable=KIWI_BYOK_UNLOCKABLE,
)
raise ValueError(
f"Feature '{feature}' requires {needed} tier. "
f"Current tier: {tier}."
)