"""Normalized Nous Portal account entitlement helpers.""" from __future__ import annotations import hashlib import json import threading import time import urllib.request from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any, Literal, Optional NousAccountInfoSource = Literal["jwt", "account_api", "inference_key", "none", "error"] # Free tool-pool coverage categories. Kept byte-for-byte aligned with the # Portal's TOOL_COVERAGE_CATEGORIES (nous-account-service # src/server/tool-pool-eligibility.ts). The Portal mints these into the # `tool_access.coverage` map on the JWT and /api/oauth/account; FAL video gen # (`fal-video`) is intentionally excluded from the pool. TOOL_COVERAGE_CATEGORIES = ( "firecrawl", "fal", "fal-video", "openai-audio", "browser-use", "modal", ) _ACCOUNT_INFO_CACHE_TTL = 60 _account_info_cache: tuple[str, float, "NousPortalAccountInfo"] | None = None _ACCOUNT_INFO_CACHE_LOCK = threading.Lock() @dataclass(frozen=True) class NousPortalSubscriptionInfo: plan: Optional[str] = None tier: Optional[int] = None monthly_charge: Optional[float] = None monthly_credits: Optional[float] = None current_period_end: Optional[str] = None credits_remaining: Optional[float] = None rollover_credits: Optional[float] = None @dataclass(frozen=True) class NousPaidServiceAccessInfo: allowed: Optional[bool] = None paid_access: Optional[bool] = None reason: Optional[str] = None organisation_id: Optional[str] = None effective_at_ms: Optional[int] = None has_active_subscription: Optional[bool] = None active_subscription_is_paid: Optional[bool] = None subscription_tier: Optional[int] = None subscription_monthly_charge: Optional[float] = None subscription_credits_remaining: Optional[float] = None purchased_credits_remaining: Optional[float] = None total_usable_credits: Optional[float] = None member_spend_cap_exceeded: Optional[bool] = None member_spend_cap_usd: Optional[float] = None member_spend_usd: Optional[float] = None member_spend_cap_remaining_usd: Optional[float] = None @dataclass(frozen=True) class NousToolAccessInfo: """Free tool-pool entitlement, decoupled from paid/billing access. Mirrors the Portal's ``tool_access`` claim/field: ``enabled`` is true when a positive tool-pool balance is live and not gated off; ``coverage`` maps each tool category to whether the pool funds it (FAL video is excluded). """ enabled: bool = False coverage: dict[str, bool] = field(default_factory=dict) @dataclass(frozen=True) class NousPortalAccountInfo: logged_in: bool source: NousAccountInfoSource fresh: bool user_id: Optional[str] = None org_id: Optional[str] = None org_slug: Optional[str] = None org_name: Optional[str] = None client_id: Optional[str] = None product_id: Optional[str] = None nous_client: Optional[str] = None portal_base_url: Optional[str] = None inference_base_url: Optional[str] = None inference_credential_present: bool = False credential_source: Optional[str] = None expires_at: Optional[datetime] = None email: Optional[str] = None privy_did: Optional[str] = None subscription: Optional[NousPortalSubscriptionInfo] = None paid_service_access: Optional[bool] = None paid_service_access_info: Optional[NousPaidServiceAccessInfo] = None tool_access: Optional[NousToolAccessInfo] = None raw_claims: Optional[dict[str, Any]] = None raw_account: Optional[dict[str, Any]] = None error: Optional[str] = None @property def is_paid(self) -> bool: return self.paid_service_access is True @property def is_free_tier(self) -> bool: return self.paid_service_access is False @property def tool_gateway_entitled(self) -> bool: """Coarse "entitled to any managed tool" gate: paid access OR a live free tool pool. Use :meth:`tool_gateway_entitled_for` to gate a specific tool category (the pool does not cover every category).""" if self.paid_service_access is True: return True return self.tool_access is not None and self.tool_access.enabled def tool_gateway_entitled_for(self, category: str) -> bool: """Whether a specific tool category is entitled. Paid users are entitled everywhere; free tool-pool users only where ``coverage[category]`` is true (e.g. image but not video).""" if self.paid_service_access is True: return True ta = self.tool_access return bool(ta and ta.enabled and ta.coverage.get(category) is True) def nous_portal_billing_url(account_info: Optional[NousPortalAccountInfo] = None) -> str: """Return the billing URL for a normalized Nous account snapshot.""" try: from hermes_cli.auth import DEFAULT_NOUS_PORTAL_URL except Exception: DEFAULT_NOUS_PORTAL_URL = "https://portal.nousresearch.com" base = None if account_info is not None: base = account_info.portal_base_url if not isinstance(base, str) or not base.strip(): base = DEFAULT_NOUS_PORTAL_URL return f"{base.rstrip('/')}/billing" def nous_portal_topup_url(account_info: Optional[NousPortalAccountInfo] = None) -> str: """Return the portal top-up URL that auto-opens the top-up modal. Prefers the org-pinned page ``{base}/orgs/{slug}/billing?topup=open`` (skips the legacy shim's re-resolution + multi-org disambiguation). Falls back to the legacy ``{base}/billing?topup=open`` when the account has no ``org_slug`` (the portal's ``slug`` is nullable; the legacy page forwards the param through to the org-pinned page). Never builds ``/orgs/None/billing``. The ``?topup=open`` query is the NAS enabler that lands the user in the top-up flow rather than just on the billing page. """ base_billing = nous_portal_billing_url(account_info) # {base}/billing base = base_billing[: -len("/billing")] # strip the trailing /billing slug = getattr(account_info, "org_slug", None) if account_info is not None else None if isinstance(slug, str) and slug.strip(): from urllib.parse import quote return f"{base}/orgs/{quote(slug.strip(), safe='')}/billing?topup=open" return f"{base}/billing?topup=open" def format_nous_portal_entitlement_message( account_info: Optional[NousPortalAccountInfo], *, capability: str = "this feature", include_refresh_hint: bool = True, coverage_category: Optional[str] = None, ) -> Optional[str]: """Return user-facing guidance for a missing Nous tool-gateway entitlement. ``None`` means the account is entitled to use the capability — via paid service access OR a live free tool pool that covers it. The message works from normalized entitlement fields rather than subscription price alone: purchased credits without a subscription still count as paid access, while a paid subscription with exhausted usable credits does not. ``coverage_category`` scopes the check to a single tool category (e.g. ``"fal-video"``). When given, a user who is entitled overall but whose access does not fund that category gets a neutral billing nudge instead of a message implying their credits are exhausted. The pool-vs-paid distinction is never surfaced to the user. """ billing_url = nous_portal_billing_url(account_info) if account_info is not None: if coverage_category is not None: if account_info.tool_gateway_entitled_for(coverage_category): return None if account_info.tool_gateway_entitled: # Entitled overall (e.g. via the managed tool pool), but this # specific capability isn't covered. Surface a neutral billing # nudge without exposing pool-vs-paid internals to the user. return ( f"{capability} isn't included with your current Nous Portal " f"access. Add credits or a subscription to enable it at {billing_url}." ) elif account_info.tool_gateway_entitled: return None if account_info is None: return ( f"Hermes could not verify your Nous Portal entitlement, so {capability} " f"is unavailable. Run `hermes model` to refresh your login, or check " f"billing at {billing_url}." ) if not account_info.logged_in: if account_info.inference_credential_present: return ( f"Nous inference credentials are configured, but Hermes cannot verify " f"your Nous Portal paid access for {capability}. Log in with " f"`hermes model` to enable Portal-managed features. Billing and " f"credits are managed at {billing_url}." ) return ( f"Log in to Nous Portal to use {capability}: run `hermes model`. " f"Billing and credits are managed at {billing_url}." ) if account_info.paid_service_access is None: detail = ( f"Hermes could not verify your Nous Portal paid access, so {capability} " f"is unavailable." ) if account_info.error: detail += f" Account lookup failed: {account_info.error}." if include_refresh_hint: detail += " Run `hermes model` to refresh your session." detail += f" Check billing at {billing_url}." return detail access = account_info.paid_service_access_info reason = access.reason if access else None if reason == "account_missing": return ( f"Hermes could not find a Nous Portal account or organisation for this " f"login, so {capability} is unavailable. Run `hermes model` to " f"authenticate again; if the problem persists, contact Nous support." ) if reason == "no_usable_credits" or account_info.paid_service_access is False: message = _no_paid_access_message(account_info, capability, billing_url) if include_refresh_hint and not account_info.fresh: message += " If you recently bought credits, run `hermes model` to refresh Hermes." return message return ( f"Your Nous Portal account does not currently have paid service access, " f"so {capability} is unavailable. Add credits or update billing at {billing_url}." ) def _no_paid_access_message( account_info: NousPortalAccountInfo, capability: str, billing_url: str, ) -> str: access = account_info.paid_service_access_info has_active_subscription = access.has_active_subscription if access else None active_subscription_is_paid = access.active_subscription_is_paid if access else None total_usable = access.total_usable_credits if access else None subscription_credits = access.subscription_credits_remaining if access else None purchased_credits = access.purchased_credits_remaining if access else None if access and access.member_spend_cap_exceeded: cap = access.member_spend_cap_usd spent = access.member_spend_usd credit_detail = _credit_detail(total_usable, subscription_credits, purchased_credits) cap_detail = "" if cap is not None and spent is not None: cap_detail = f" Your organisation's per-member spend cap is ${cap:.2f} and you've spent ${spent:.2f} of it." elif cap is not None: cap_detail = f" Your organisation's per-member spend cap is ${cap:.2f}." return ( f"Your Nous Portal access is paused because you've exceeded the" f" per-member spend cap set by your organisation.{cap_detail}" f"{credit_detail} Ask your organisation admin to raise the" f" member spend cap at {billing_url}, then run `hermes model`" f" to refresh." ) if has_active_subscription and active_subscription_is_paid: credit_detail = _credit_detail(total_usable, subscription_credits, purchased_credits) return ( f"Your Nous Portal credits are exhausted{credit_detail}, so {capability} " f"is unavailable. Top up or renew credits at {billing_url}." ) if has_active_subscription and active_subscription_is_paid is False: return ( f"Your current Nous Portal plan does not include paid service access, " f"so {capability} is unavailable. Upgrade or add credits at {billing_url}." ) if has_active_subscription is False: credit_detail = _credit_detail(total_usable, subscription_credits, purchased_credits) return ( f"Your Nous Portal account has no active subscription or usable credits" f"{credit_detail}, so {capability} is unavailable. Subscribe or add credits " f"at {billing_url}." ) credit_detail = _credit_detail(total_usable, subscription_credits, purchased_credits) return ( f"Your Nous Portal account has no usable paid credits{credit_detail}, so " f"{capability} is unavailable. Add credits or update billing at {billing_url}." ) def _credit_detail( total_usable: Optional[float], subscription_credits: Optional[float], purchased_credits: Optional[float], ) -> str: parts: list[str] = [] if total_usable is not None: parts.append(f"usable ${total_usable:.2f}") if subscription_credits is not None: parts.append(f"subscription ${subscription_credits:.2f}") if purchased_credits is not None: parts.append(f"purchased ${purchased_credits:.2f}") if not parts: return "" return f" ({', '.join(parts)})" def reset_nous_portal_account_info_cache() -> None: """Clear the short-lived account-info cache used by tests.""" global _account_info_cache _account_info_cache = None def get_nous_portal_account_info( *, force_fresh: bool = False, min_jwt_ttl_seconds: int = 60, ) -> NousPortalAccountInfo: """Return normalized Nous Portal account entitlement information. By default, a valid unexpired OAuth access JWT is used as a low-latency local account snapshot. ``force_fresh=True`` always calls ``/api/oauth/account`` and bypasses the short-lived cache. JWT claims are decoded locally for UX gating only; server APIs remain authoritative. """ try: from hermes_cli.auth import get_provider_auth_state state = get_provider_auth_state("nous") or {} except Exception as exc: return _error_info(error=exc, logged_in=False) access_token = state.get("access_token") portal_base_url = _portal_base_url(state) if not isinstance(access_token, str) or not access_token.strip(): pool_oauth_info = _info_from_oauth_pool( force_fresh=force_fresh, min_jwt_ttl_seconds=min_jwt_ttl_seconds, portal_base_url=portal_base_url, ) if pool_oauth_info is not None: return pool_oauth_info pool_info = _info_from_inference_key_pool(portal_base_url) if pool_info is not None: return pool_info return NousPortalAccountInfo( logged_in=False, source="none", fresh=False, portal_base_url=portal_base_url, ) if not force_fresh: jwt_info = _info_from_valid_jwt( access_token, state=state, portal_base_url=portal_base_url, min_jwt_ttl_seconds=min_jwt_ttl_seconds, ) if jwt_info is not None: return jwt_info return _fresh_account_info( state=state, force_fresh=force_fresh, portal_base_url=portal_base_url, ) def nous_policy_present() -> Optional[bool]: """Whether the caller's org carries a restrictive model/provider policy. Reads the ``policy_present`` claim off the access token, so it costs no request; ``/api/oauth/account`` does not carry it. Stamped at mint time, so it goes stale until the next token refresh. ``None`` is unknown — an older mint or an unreadable claim — and must not be reported as the absence of a policy. """ try: from hermes_cli.auth import get_provider_auth_state, _decode_jwt_claims state = get_provider_auth_state("nous") or {} access_token = state.get("access_token") if not isinstance(access_token, str) or not access_token.strip(): return None claims = _decode_jwt_claims(access_token) if not claims: return None return _coerce_bool(claims.get("policy_present")) except Exception: return None def nous_policy_notice(*, removed: bool) -> str: """A one-line notice for a list the org's policy narrowed, else ``""``. A blocked model is omitted rather than marked, which reads as "Hermes does not support this". This says which it is without enumerating the blocked set, which under an allowlist is most of the catalog. *removed* is whether the filter actually dropped anything. The catalog read fails open — an anonymous or empty one narrows nothing — so the claim alone would label a full list as filtered. """ if not removed or nous_policy_present() is not True: return "" return ( "Your organization restricts which models are available — " "models outside its policy are not listed." ) def _fresh_account_info( *, state: dict[str, Any], force_fresh: bool, portal_base_url: Optional[str], ) -> NousPortalAccountInfo: global _account_info_cache try: from hermes_cli.auth import get_provider_auth_state, resolve_nous_access_token access_token = resolve_nous_access_token() refreshed_state = get_provider_auth_state("nous") or state portal_base_url = _portal_base_url(refreshed_state) or portal_base_url cache_key = _cache_key(access_token, portal_base_url) with _ACCOUNT_INFO_CACHE_LOCK: if not force_fresh and _account_info_cache is not None: cached_key, cached_at, cached_info = _account_info_cache if cached_key == cache_key and (time.monotonic() - cached_at) < _ACCOUNT_INFO_CACHE_TTL: return cached_info payload = _fetch_nous_account_info(access_token, portal_base_url) if not payload: return _error_info( error="empty_account_response", logged_in=True, portal_base_url=portal_base_url, ) if isinstance(payload.get("error"), str): return _error_info( error=payload.get("error") or "account_response_error", logged_in=True, portal_base_url=portal_base_url, raw_account=payload, ) info = _info_from_account_payload( payload, state=refreshed_state, portal_base_url=portal_base_url, ) with _ACCOUNT_INFO_CACHE_LOCK: _account_info_cache = (cache_key, time.monotonic(), info) return info except Exception as exc: return _error_info( error=exc, logged_in=bool(state.get("access_token")), portal_base_url=portal_base_url, ) def _info_from_inference_key_pool( portal_base_url: Optional[str], ) -> Optional[NousPortalAccountInfo]: """Return an explicit unknown-entitlement snapshot for opaque Nous keys.""" try: entry = _select_nous_pool_entry() if entry is None: return None runtime_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "") if not isinstance(runtime_key, str) or not runtime_key.strip(): return None return NousPortalAccountInfo( logged_in=False, source="inference_key", fresh=False, portal_base_url=( getattr(entry, "portal_base_url", None) or portal_base_url ), inference_base_url=( getattr(entry, "inference_base_url", None) or getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) ), inference_credential_present=True, credential_source=f"pool:{getattr(entry, 'label', 'unknown')}", error="portal_oauth_missing", ) except Exception: return None def _info_from_oauth_pool( *, force_fresh: bool, min_jwt_ttl_seconds: int, portal_base_url: Optional[str], ) -> Optional[NousPortalAccountInfo]: try: entry = _select_nous_pool_entry() except Exception: return None if entry is None or not _pool_entry_is_portal_oauth(entry): return None access_token = getattr(entry, "access_token", None) if not isinstance(access_token, str) or not access_token.strip(): return None entry_portal_url = ( getattr(entry, "portal_base_url", None) or portal_base_url ) state = { "access_token": access_token, "client_id": getattr(entry, "client_id", None), "inference_base_url": ( getattr(entry, "inference_base_url", None) or getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) ), "agent_key": getattr(entry, "agent_key", None), "credential_source": f"pool:{getattr(entry, 'label', 'unknown')}", } if not force_fresh: jwt_info = _info_from_valid_jwt( access_token, state=state, portal_base_url=entry_portal_url, min_jwt_ttl_seconds=min_jwt_ttl_seconds, ) if jwt_info is not None: return jwt_info try: payload = _fetch_nous_account_info(access_token, entry_portal_url) except Exception as exc: return _error_info( error=exc, logged_in=True, portal_base_url=entry_portal_url, ) if not payload: return _error_info( error="empty_account_response", logged_in=True, portal_base_url=entry_portal_url, ) if isinstance(payload.get("error"), str): return _error_info( error=payload.get("error") or "account_response_error", logged_in=True, portal_base_url=entry_portal_url, raw_account=payload, ) return _info_from_account_payload( payload, state=state, portal_base_url=entry_portal_url, ) def _select_nous_pool_entry() -> Optional[Any]: from agent.credential_pool import load_pool pool = load_pool("nous") if not pool or not pool.has_credentials(): return None entries = list(pool.entries()) if not entries: return None def _entry_sort_key(entry: Any) -> tuple[float, float, int]: agent_exp = _parse_iso_timestamp(getattr(entry, "agent_key_expires_at", None)) or 0.0 access_exp = _parse_iso_timestamp(getattr(entry, "expires_at", None)) or 0.0 priority = int(getattr(entry, "priority", 0) or 0) return (agent_exp, access_exp, -priority) return max(entries, key=_entry_sort_key) def _pool_entry_is_portal_oauth(entry: Any) -> bool: access_token = getattr(entry, "access_token", None) if not isinstance(access_token, str) or not access_token.strip(): return False auth_type = str(getattr(entry, "auth_type", "") or "").strip().lower() refresh_token = getattr(entry, "refresh_token", None) return auth_type.startswith("oauth") or bool(refresh_token) def _fetch_nous_account_info( access_token: str, portal_base_url: Optional[str] = None, ) -> dict[str, Any]: base = (portal_base_url or "https://portal.nousresearch.com").rstrip("/") url = f"{base}/api/oauth/account" headers = { "Authorization": f"Bearer {access_token}", "Accept": "application/json", } req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=8) as resp: payload = json.loads(resp.read().decode()) return payload if isinstance(payload, dict) else {} def _info_from_valid_jwt( token: str, *, state: dict[str, Any], portal_base_url: Optional[str], min_jwt_ttl_seconds: int, ) -> Optional[NousPortalAccountInfo]: try: from hermes_cli.auth import _decode_jwt_claims except Exception: return None claims = _decode_jwt_claims(token) if not claims: return None exp = _coerce_float(claims.get("exp")) if exp is None or exp <= time.time() + max(0, int(min_jwt_ttl_seconds)): return None paid_access = _coerce_bool(claims.get("paid_access")) subscription_tier = _coerce_int(claims.get("subscription_tier")) access_info = NousPaidServiceAccessInfo( allowed=paid_access, paid_access=paid_access, organisation_id=_coerce_str(claims.get("org_id")), subscription_tier=subscription_tier, ) return NousPortalAccountInfo( logged_in=True, source="jwt", fresh=False, user_id=_coerce_str(claims.get("sub")), org_id=_coerce_str(claims.get("org_id")), client_id=_coerce_str(claims.get("client_id") or state.get("client_id")), product_id=_coerce_str(claims.get("product_id")), nous_client=_coerce_str(claims.get("nous_client")), portal_base_url=portal_base_url, inference_base_url=_coerce_str(state.get("inference_base_url")), inference_credential_present=True, credential_source=_coerce_str(state.get("credential_source")) or "auth_store", expires_at=datetime.fromtimestamp(exp, tz=timezone.utc), paid_service_access=paid_access, paid_service_access_info=access_info, tool_access=_tool_access_from_value(claims.get("tool_access")), raw_claims=dict(claims), ) def _info_from_account_payload( payload: dict[str, Any], *, state: dict[str, Any], portal_base_url: Optional[str], ) -> NousPortalAccountInfo: raw_user = payload.get("user") user: dict[str, Any] = raw_user if isinstance(raw_user, dict) else {} raw_org = payload.get("organisation") organisation: dict[str, Any] = raw_org if isinstance(raw_org, dict) else {} subscription = _subscription_from_payload(payload.get("subscription")) access = _paid_service_access_from_payload(payload.get("paid_service_access")) paid_access = access.allowed if access else None if paid_access is None and access is not None: paid_access = access.paid_access return NousPortalAccountInfo( logged_in=True, source="account_api", fresh=True, org_id=_coerce_str(organisation.get("id")) or (access.organisation_id if access else None), org_slug=_coerce_str(organisation.get("slug")), org_name=_coerce_str(organisation.get("name")), client_id=_coerce_str(state.get("client_id")), portal_base_url=portal_base_url, inference_base_url=_coerce_str(state.get("inference_base_url")), inference_credential_present=bool(state.get("access_token") or state.get("agent_key")), credential_source=_coerce_str(state.get("credential_source")) or "auth_store", email=_coerce_str(user.get("email")), privy_did=_coerce_str(user.get("privy_did")), subscription=subscription, paid_service_access=paid_access, paid_service_access_info=access, tool_access=_tool_access_from_value(payload.get("tool_access")), raw_account=dict(payload), ) def _tool_access_from_value(value: Any) -> Optional[NousToolAccessInfo]: """Parse a Portal ``tool_access`` object (from the JWT claim or the account API) into :class:`NousToolAccessInfo`. Fails closed: a non-object value yields ``None``, and only literal ``true`` counts for ``enabled`` and each coverage entry.""" if not isinstance(value, dict): return None enabled = _coerce_bool(value.get("enabled")) is True raw_coverage = value.get("coverage") coverage: dict[str, bool] = {} if isinstance(raw_coverage, dict): for key, val in raw_coverage.items(): if isinstance(key, str): coverage[key] = val is True return NousToolAccessInfo(enabled=enabled, coverage=coverage) def _subscription_from_payload(value: Any) -> Optional[NousPortalSubscriptionInfo]: if not isinstance(value, dict): return None return NousPortalSubscriptionInfo( plan=_coerce_str(value.get("plan")), tier=_coerce_int(value.get("tier")), monthly_charge=_coerce_float(value.get("monthly_charge")), monthly_credits=_coerce_float(value.get("monthly_credits")), current_period_end=_coerce_str(value.get("current_period_end")), credits_remaining=_coerce_float(value.get("credits_remaining")), rollover_credits=_coerce_float(value.get("rollover_credits")), ) def _paid_service_access_from_payload(value: Any) -> Optional[NousPaidServiceAccessInfo]: if not isinstance(value, dict): return None allowed = _coerce_bool(value.get("allowed")) paid_access = _coerce_bool(value.get("paid_access")) return NousPaidServiceAccessInfo( allowed=allowed, paid_access=paid_access, reason=_coerce_str(value.get("reason")), organisation_id=_coerce_str(value.get("organisation_id")), effective_at_ms=_coerce_int(value.get("effective_at_ms")), has_active_subscription=_coerce_bool(value.get("has_active_subscription")), active_subscription_is_paid=_coerce_bool(value.get("active_subscription_is_paid")), subscription_tier=_coerce_int(value.get("subscription_tier")), subscription_monthly_charge=_coerce_float(value.get("subscription_monthly_charge")), subscription_credits_remaining=_coerce_float(value.get("subscription_credits_remaining")), purchased_credits_remaining=_coerce_float(value.get("purchased_credits_remaining")), total_usable_credits=_coerce_float(value.get("total_usable_credits")), member_spend_cap_exceeded=_coerce_bool(value.get("member_spend_cap_exceeded")), member_spend_cap_usd=_coerce_float(value.get("member_spend_cap_usd")), member_spend_usd=_coerce_float(value.get("member_spend_usd")), member_spend_cap_remaining_usd=_coerce_float(value.get("member_spend_cap_remaining_usd")), ) def _error_info( *, error: object, logged_in: bool, portal_base_url: Optional[str] = None, raw_account: Optional[dict[str, Any]] = None, ) -> NousPortalAccountInfo: return NousPortalAccountInfo( logged_in=logged_in, source="error", fresh=False, portal_base_url=portal_base_url, raw_account=raw_account, error=str(error), ) def _portal_base_url(state: dict[str, Any]) -> Optional[str]: value = state.get("portal_base_url") if not isinstance(value, str) or not value.strip(): return None return value.strip().rstrip("/") def _cache_key(access_token: str, portal_base_url: Optional[str]) -> str: digest = hashlib.sha256(access_token.encode("utf-8")).hexdigest() return f"{portal_base_url or ''}:{digest}" def _parse_iso_timestamp(value: Any) -> Optional[float]: if not isinstance(value, str) or not value: return None text = value.strip() if text.endswith("Z"): text = text[:-1] + "+00:00" try: return datetime.fromisoformat(text).timestamp() except Exception: return None def _coerce_str(value: Any) -> Optional[str]: if isinstance(value, str) and value: return value return None def _coerce_bool(value: Any) -> Optional[bool]: return value if isinstance(value, bool) else None def _coerce_int(value: Any) -> Optional[int]: if isinstance(value, bool): return None try: if value is None: return None return int(value) except (TypeError, ValueError): return None def _coerce_float(value: Any) -> Optional[float]: if isinstance(value, bool): return None try: if value is None: return None return float(value) except (TypeError, ValueError): return None