kiwi/app/tiers.py
pyr0ball c18bfec8f5
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
fix(lint): add ruff config, fix all lint errors for GitHub CI
- pyproject.toml: add [tool.ruff] config suppressing E402/W293 globally,
  F841/E741/E702 in tests, E741 in scripts
- inventory.py: split semicolon import (E702)
- recipes.py: fix logger -> log (F821 undefined name)
- shopping.py: rename l -> lnk in list comprehension (E741)
- format_conversion.py: noqa F841 on CUDA flag (used as future hook)
- backfill_keywords.py: rename done -> _done (F841)
- ingest_purplecarrot.py: drop == True comparison (E712)
- Auto-fix: I001 import sorting, F401 unused imports across all files
2026-07-06 02:49:02 -07:00

99 lines
3.5 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
# Features that unlock when the user supplies their own LLM backend.
KIWI_BYOK_UNLOCKABLE: frozenset[str] = frozenset({
"recipe_suggestions",
"expiry_llm_matching",
"receipt_ocr",
"recipe_scan",
"style_classifier",
"meal_plan_llm",
"meal_plan_llm_timing",
"community_fork_adapt",
})
# Sources subject to monthly cf-orch call caps. Subscription-based sources are uncapped.
LIFETIME_SOURCES: frozenset[str] = frozenset({"lifetime", "founders"})
# (source, tier) → monthly cf-orch call allowance
LIFETIME_ORCH_CAPS: dict[tuple[str, str], int] = {
("lifetime", "paid"): 60,
("lifetime", "premium"): 180,
("founders", "premium"): 300,
}
# 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
"visual_label_capture": "paid", # Camera capture for unenriched barcodes (kiwi#79)
"recipe_suggestions": "paid", # BYOK-unlockable
"expiry_llm_matching": "paid", # BYOK-unlockable
"meal_planning": "free",
"meal_plan_config": "paid", # configurable meal types (breakfast/lunch/snack)
"dietary_profiles": "paid",
"style_picker": "paid",
"recipe_collections": "paid",
"community_publish": "paid", # Publish plans/outcomes to community feed
# Premium tier — heavy LLM features (model call per request)
"meal_plan_llm": "premium", # LLM-assisted full-week plan generation; BYOK-unlockable
"meal_plan_llm_timing": "premium", # LLM time fill-in for recipes missing corpus times; BYOK-unlockable
"style_classifier": "premium", # LLM auto-tag for saved recipe style tags; BYOK-unlockable
"community_fork_adapt": "premium", # Fork with LLM pantry adaptation; BYOK-unlockable
"recipe_scan": "premium", # BYOK-unlockable: photo -> structured recipe
"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}."
)