From 3dc3809d07be8b09091982ecadc5e08cfd40e2f6 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 9 Sep 2026 05:12:07 -0700 Subject: [PATCH] refactor(fal): render the managed billing message once in fal_common Image and video callers were each formatting the same four-field dict into the same sentence. Return the rendered tail from _managed_fal_billing_error so the wording lives in one place; output is byte-identical. --- plugins/video_gen/fal/__init__.py | 10 ++-------- tools/fal_common.py | 28 ++++++++++++++++------------ tools/image_generation_tool.py | 10 ++-------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/plugins/video_gen/fal/__init__.py b/plugins/video_gen/fal/__init__.py index 94841727e2..3b126a7008 100644 --- a/plugins/video_gen/fal/__init__.py +++ b/plugins/video_gen/fal/__init__.py @@ -224,16 +224,10 @@ def _submit_fal_video_request(endpoint: str, arguments: Dict[str, Any]): from tools.fal_common import _extract_http_status, _managed_fal_billing_error status = _extract_http_status(exc) if status is not None and 400 <= status < 500: - billing = _managed_fal_billing_error(exc) + billing = _managed_fal_billing_error(exc, "endpoint") if billing is not None: raise ValueError( - f"Nous Subscription gateway rejected endpoint '{endpoint}' (HTTP {status}): " - f"{billing['message']} ({billing['error_code']}; " - f"{billing['code']}: {billing['detail']}). " - "This is a Nous Portal billing configuration issue, not a missing local API key. " - "The managed route cannot run this endpoint until Nous enables its billing meter; " - "a direct FAL_KEY is an optional bypass." - ) from exc + f"Nous Subscription gateway rejected endpoint '{endpoint}' (HTTP {status}): {billing}") from exc raise ValueError(f"Nous Subscription gateway rejected endpoint '{endpoint}' (HTTP {status}). This model may not yet be enabled " f"on the Nous Portal's FAL proxy. Either:\n • Set FAL_KEY in your environment to use FAL.ai directly, or\n" f" • Pick a different model via `hermes tools` → Video Generation.") from exc diff --git a/tools/fal_common.py b/tools/fal_common.py index 2410cfd295..d1918a019d 100644 --- a/tools/fal_common.py +++ b/tools/fal_common.py @@ -46,8 +46,12 @@ def _extract_http_status(exc: BaseException) -> Optional[int]: return status if isinstance(status, int) else None -def _managed_fal_billing_error(exc: BaseException) -> Optional[Dict[str, str]]: - """Return normalized Nous billing details from a managed-gateway error.""" +def _managed_fal_billing_error(exc: BaseException, what: str) -> Optional[str]: + """Human-readable tail for a Nous managed-gateway ``BILLING_ERROR`` response, else None. + + ``what`` names the rejected thing ("model", "endpoint"); the wording is shared by the image + and video callers so the two surfaces never drift. + """ response = getattr(exc, "response", None) if response is None: return None @@ -58,16 +62,16 @@ def _managed_fal_billing_error(exc: BaseException) -> Optional[Dict[str, str]]: error = payload.get("error") if isinstance(payload, dict) else None if not isinstance(error, dict) or error.get("code") != "BILLING_ERROR": return None - raw_details = error.get("details") - details = raw_details if isinstance(raw_details, dict) else {} - raw_upstream = details.get("upstreamPayload") - upstream = raw_upstream if isinstance(raw_upstream, dict) else {} - return { - "message": str(error.get("message") or "Charge authorization failed"), - "error_code": str(error.get("code") or "BILLING_ERROR"), - "code": str(upstream.get("code") or details.get("chargeIntentErrorCode") or "billing_error"), - "detail": str(upstream.get("error") or "Nous Portal rejected the charge authorization"), - } + details = error.get("details") if isinstance(error.get("details"), dict) else {} + upstream = details.get("upstreamPayload") if isinstance(details.get("upstreamPayload"), dict) else {} + code = upstream.get("code") or details.get("chargeIntentErrorCode") or "billing_error" + detail = upstream.get("error") or "Nous Portal rejected the charge authorization" + return ( + f"{error.get('message') or 'Charge authorization failed'} (BILLING_ERROR; {code}: {detail}). " + "This is a Nous Portal billing configuration issue, not a missing local API key. " + f"The managed route cannot run this {what} until Nous enables its billing meter; " + "a direct FAL_KEY is an optional bypass." + ) def _require(value: Any, what: str) -> Any: diff --git a/tools/image_generation_tool.py b/tools/image_generation_tool.py index 328d4dcd14..887d390e83 100644 --- a/tools/image_generation_tool.py +++ b/tools/image_generation_tool.py @@ -134,16 +134,10 @@ def _submit_fal_request(model: str, arguments: Dict[str, Any]): # (allowlist miss, billing gate): give remediation instead of a raw httpx error. status = _extract_http_status(exc) if status is not None and 400 <= status < 500: - billing = _managed_fal_billing_error(exc) + billing = _managed_fal_billing_error(exc, "model") if billing is not None: raise ValueError( - f"Nous Subscription gateway rejected model '{model}' (HTTP {status}): " - f"{billing['message']} ({billing['error_code']}; " - f"{billing['code']}: {billing['detail']}). " - "This is a Nous Portal billing configuration issue, not a missing local API key. " - "The managed route cannot run this model until Nous enables its billing meter; " - "a direct FAL_KEY is an optional bypass." - ) from exc + f"Nous Subscription gateway rejected model '{model}' (HTTP {status}): {billing}") from exc gateway_message = "" if status in {401, 402, 403}: gateway_message = "\n\n" + nous_tool_gateway_unavailable_message(