From 2e722ee29ae2acebe2051b35303eb4a29f7cfcfc Mon Sep 17 00:00:00 2001 From: Teknium Date: Tue, 21 Apr 2026 01:59:15 -0700 Subject: [PATCH] fix(fal): extend whitespace-only FAL_KEY handling to all call sites Follow-up to PR #2504. The original fix covered the two direct FAL_KEY checks in image_generation_tool but left four other call sites intact, including the managed-gateway gate where a whitespace-only FAL_KEY falsely claimed 'user has direct FAL' and *skipped* the Nous managed gateway fallback entirely. Introduce fal_key_is_configured() in tools/tool_backend_helpers.py as a single source of truth (consults os.environ, falls back to .env for CLI-setup paths) and route every FAL_KEY presence check through it: - tools/image_generation_tool.py : _resolve_managed_fal_gateway, image_generate_tool's upfront check, check_fal_api_key - hermes_cli/nous_subscription.py : direct_fal detection, selected toolset gating, tools_ready map - hermes_cli/tools_config.py : image_gen needs-setup check Verified by extending tests/tools/test_image_generation_env.py and by E2E exercising whitespace + managed-gateway composition directly. --- hermes_cli/nous_subscription.py | 7 ++++--- hermes_cli/tools_config.py | 4 ++-- tools/image_generation_tool.py | 16 ++++++++-------- tools/tool_backend_helpers.py | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/hermes_cli/nous_subscription.py b/hermes_cli/nous_subscription.py index a4883b056..78181aab2 100644 --- a/hermes_cli/nous_subscription.py +++ b/hermes_cli/nous_subscription.py @@ -10,6 +10,7 @@ from hermes_cli.auth import get_nous_auth_status from hermes_cli.config import get_env_value, load_config from tools.managed_tool_gateway import is_managed_tool_gateway_ready from tools.tool_backend_helpers import ( + fal_key_is_configured, has_direct_modal_credentials, managed_nous_tools_enabled, normalize_browser_cloud_provider, @@ -271,7 +272,7 @@ def get_nous_subscription_features( direct_firecrawl = bool(get_env_value("FIRECRAWL_API_KEY") or get_env_value("FIRECRAWL_API_URL")) direct_parallel = bool(get_env_value("PARALLEL_API_KEY")) direct_tavily = bool(get_env_value("TAVILY_API_KEY")) - direct_fal = bool(get_env_value("FAL_KEY")) + direct_fal = fal_key_is_configured() direct_openai_tts = bool(resolve_openai_audio_api_key()) direct_elevenlabs = bool(get_env_value("ELEVENLABS_API_KEY")) direct_camofox = bool(get_env_value("CAMOFOX_URL")) @@ -520,7 +521,7 @@ def apply_nous_managed_defaults( browser_cfg["cloud_provider"] = "browser-use" changed.add("browser") - if "image_gen" in selected_toolsets and not get_env_value("FAL_KEY"): + if "image_gen" in selected_toolsets and not fal_key_is_configured(): changed.add("image_gen") return changed @@ -548,7 +549,7 @@ def _get_gateway_direct_credentials() -> Dict[str, bool]: or get_env_value("TAVILY_API_KEY") or get_env_value("EXA_API_KEY") ), - "image_gen": bool(get_env_value("FAL_KEY")), + "image_gen": fal_key_is_configured(), "tts": bool( resolve_openai_audio_api_key() or get_env_value("ELEVENLABS_API_KEY") diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 24c5fde5f..36b3c7f3f 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -24,7 +24,7 @@ from hermes_cli.nous_subscription import ( apply_nous_managed_defaults, get_nous_subscription_features, ) -from tools.tool_backend_helpers import managed_nous_tools_enabled +from tools.tool_backend_helpers import fal_key_is_configured, managed_nous_tools_enabled from utils import base_url_hostname logger = logging.getLogger(__name__) @@ -876,7 +876,7 @@ def _toolset_needs_configuration_prompt(ts_key: str, config: dict) -> bool: browser_cfg = config.get("browser", {}) return not isinstance(browser_cfg, dict) or "cloud_provider" not in browser_cfg if ts_key == "image_gen": - return not get_env_value("FAL_KEY") + return not fal_key_is_configured() return not _toolset_has_keys(ts_key, config) diff --git a/tools/image_generation_tool.py b/tools/image_generation_tool.py index e10b8453c..13e95ef2d 100644 --- a/tools/image_generation_tool.py +++ b/tools/image_generation_tool.py @@ -33,7 +33,11 @@ import fal_client from tools.debug_helpers import DebugSession from tools.managed_tool_gateway import resolve_managed_tool_gateway -from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway +from tools.tool_backend_helpers import ( + fal_key_is_configured, + managed_nous_tools_enabled, + prefers_gateway, +) logger = logging.getLogger(__name__) @@ -286,7 +290,7 @@ _managed_fal_client_lock = threading.Lock() def _resolve_managed_fal_gateway(): """Return managed fal-queue gateway config when the user prefers the gateway or direct FAL credentials are absent.""" - if os.getenv("FAL_KEY") and not prefers_gateway("image_gen"): + if fal_key_is_configured() and not prefers_gateway("image_gen"): return None return resolve_managed_tool_gateway("fal-queue") @@ -623,9 +627,7 @@ def image_generate_tool( if not prompt or not isinstance(prompt, str) or len(prompt.strip()) == 0: raise ValueError("Prompt is required and must be a non-empty string") - fal_key_value = os.getenv("FAL_KEY") - fal_key_set = bool(fal_key_value and fal_key_value.strip()) - if not (fal_key_set or _resolve_managed_fal_gateway()): + if not (fal_key_is_configured() or _resolve_managed_fal_gateway()): message = "FAL_KEY environment variable not set" if managed_nous_tools_enabled(): message += " and managed FAL gateway is unavailable" @@ -736,9 +738,7 @@ def image_generate_tool( def check_fal_api_key() -> bool: """True if the FAL.ai API key (direct or managed gateway) is available.""" - fal_key_value = os.getenv("FAL_KEY") - fal_key_set = bool(fal_key_value and fal_key_value.strip()) - return bool(fal_key_set or _resolve_managed_fal_gateway()) + return bool(fal_key_is_configured() or _resolve_managed_fal_gateway()) def check_image_generation_requirements() -> bool: diff --git a/tools/tool_backend_helpers.py b/tools/tool_backend_helpers.py index a770fe747..810a51c63 100644 --- a/tools/tool_backend_helpers.py +++ b/tools/tool_backend_helpers.py @@ -119,3 +119,24 @@ def prefers_gateway(config_section: str) -> bool: except Exception: pass return False + + +def fal_key_is_configured() -> bool: + """Return True when FAL_KEY is set to a non-whitespace value. + + Consults both ``os.environ`` and ``~/.hermes/.env`` (via + ``hermes_cli.config.get_env_value`` when available) so tool-side + checks and CLI setup-time checks agree. A whitespace-only value + is treated as unset everywhere. + """ + value = os.getenv("FAL_KEY") + if value is None: + # Fall back to the .env file for CLI paths that may run before + # dotenv is loaded into os.environ. + try: + from hermes_cli.config import get_env_value + + value = get_env_value("FAL_KEY") + except Exception: + value = None + return bool(value and value.strip())