# app/tiers.py — tier gates for Sparrow # # BYOK does not apply to Sparrow — "bring your own" means bring your own # hardware (self-hosting). is_self_hosted() checks the cf-orch URL target. from __future__ import annotations import os from fastapi import HTTPException def is_self_hosted() -> bool: """ True when cf-orch is on localhost or a LAN address. Self-hosted users get unrestricted access equivalent to Paid tier. Cloud users are gated by the standard tier model. """ orch_url = os.environ.get("CF_ORCH_URL", "") if not orch_url: return True # no orch = local dev mode = self-hosted host = orch_url.split("//")[-1].split(":")[0].split("/")[0] return host in ("localhost", "127.0.0.1") or host.startswith("10.") or \ host.startswith("192.168.") or host.startswith("172.") def require_tier(feature: str, tier: str | None = None) -> None: """ Gate a feature by tier. Raises 402 if the caller does not qualify. For self-hosted instances all features are unlocked. tier=None means the request carries no tier — treat as Free. """ if is_self_hosted(): return _TIER_RANK = {"free": 0, "paid": 1, "premium": 2} _FEATURE_MIN: dict[str, str] = { "stems": "paid", "parallel_branch": "paid", "session_allocation": "premium", "priority_queue": "premium", } min_tier = _FEATURE_MIN.get(feature, "free") caller_rank = _TIER_RANK.get(tier or "free", 0) required_rank = _TIER_RANK[min_tier] if caller_rank < required_rank: raise HTTPException( status_code=402, detail=f"Feature '{feature}' requires {min_tier} tier.", )