feat(kiwi): add orch_fallback field to RecipeResult

This commit is contained in:
pyr0ball 2026-04-14 14:38:37 -07:00
parent fbae9ced72
commit b4f031e87d
2 changed files with 12 additions and 8 deletions

View file

@ -92,6 +92,7 @@ class CloudUser:
has_byok: bool # True if a configured LLM backend is present in llm.yaml has_byok: bool # True if a configured LLM backend is present in llm.yaml
household_id: str | None = None household_id: str | None = None
is_household_owner: bool = False is_household_owner: bool = False
license_key: str | None = None # key_display for lifetime/founders keys; None for subscription/free
# ── JWT validation ───────────────────────────────────────────────────────────── # ── JWT validation ─────────────────────────────────────────────────────────────
@ -132,16 +133,16 @@ def _ensure_provisioned(user_id: str) -> None:
log.warning("Heimdall provision failed for user %s: %s", user_id, exc) log.warning("Heimdall provision failed for user %s: %s", user_id, exc)
def _fetch_cloud_tier(user_id: str) -> tuple[str, str | None, bool]: def _fetch_cloud_tier(user_id: str) -> tuple[str, str | None, bool, str | None]:
"""Returns (tier, household_id | None, is_household_owner).""" """Returns (tier, household_id | None, is_household_owner, license_key | None)."""
now = time.monotonic() now = time.monotonic()
cached = _TIER_CACHE.get(user_id) cached = _TIER_CACHE.get(user_id)
if cached and (now - cached[1]) < _TIER_CACHE_TTL: if cached and (now - cached[1]) < _TIER_CACHE_TTL:
entry = cached[0] entry = cached[0]
return entry["tier"], entry.get("household_id"), entry.get("is_household_owner", False) return entry["tier"], entry.get("household_id"), entry.get("is_household_owner", False), entry.get("license_key")
if not HEIMDALL_ADMIN_TOKEN: if not HEIMDALL_ADMIN_TOKEN:
return "free", None, False return "free", None, False, None
try: try:
resp = requests.post( resp = requests.post(
f"{HEIMDALL_URL}/admin/cloud/resolve", f"{HEIMDALL_URL}/admin/cloud/resolve",
@ -153,12 +154,13 @@ def _fetch_cloud_tier(user_id: str) -> tuple[str, str | None, bool]:
tier = data.get("tier", "free") tier = data.get("tier", "free")
household_id = data.get("household_id") household_id = data.get("household_id")
is_owner = data.get("is_household_owner", False) is_owner = data.get("is_household_owner", False)
license_key = data.get("key_display")
except Exception as exc: except Exception as exc:
log.warning("Heimdall tier resolve failed for user %s: %s", user_id, exc) log.warning("Heimdall tier resolve failed for user %s: %s", user_id, exc)
tier, household_id, is_owner = "free", None, False tier, household_id, is_owner, license_key = "free", None, False, None
_TIER_CACHE[user_id] = ({"tier": tier, "household_id": household_id, "is_household_owner": is_owner}, now) _TIER_CACHE[user_id] = ({"tier": tier, "household_id": household_id, "is_household_owner": is_owner, "license_key": license_key}, now)
return tier, household_id, is_owner return tier, household_id, is_owner, license_key
def _user_db_path(user_id: str, household_id: str | None = None) -> Path: def _user_db_path(user_id: str, household_id: str | None = None) -> Path:
@ -250,7 +252,7 @@ def get_session(request: Request) -> CloudUser:
user_id = validate_session_jwt(token) user_id = validate_session_jwt(token)
_ensure_provisioned(user_id) _ensure_provisioned(user_id)
tier, household_id, is_household_owner = _fetch_cloud_tier(user_id) tier, household_id, is_household_owner, license_key = _fetch_cloud_tier(user_id)
return CloudUser( return CloudUser(
user_id=user_id, user_id=user_id,
tier=tier, tier=tier,
@ -258,6 +260,7 @@ def get_session(request: Request) -> CloudUser:
has_byok=has_byok, has_byok=has_byok,
household_id=household_id, household_id=household_id,
is_household_owner=is_household_owner, is_household_owner=is_household_owner,
license_key=license_key,
) )

View file

@ -56,6 +56,7 @@ class RecipeResult(BaseModel):
grocery_links: list[GroceryLink] = Field(default_factory=list) grocery_links: list[GroceryLink] = Field(default_factory=list)
rate_limited: bool = False rate_limited: bool = False
rate_limit_count: int = 0 rate_limit_count: int = 0
orch_fallback: bool = False # True when orch budget exhausted; fell back to local LLM
class NutritionFilters(BaseModel): class NutritionFilters(BaseModel):