Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions tests/tools/test_image_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Tests for image_generation_tool — availability probe and fail-fast handler."""

import json
import os
from unittest.mock import patch


# ---------------------------------------------------------------------------
# detect_image_generation_environment
# ---------------------------------------------------------------------------


class TestDetectImageGenerationEnvironment:

def test_available_when_key_and_client_present(self):
with patch("tools.image_generation_tool._HAS_FAL_CLIENT", True), \
patch.dict(os.environ, {"FAL_KEY": "test-key"}):
from tools.image_generation_tool import detect_image_generation_environment
result = detect_image_generation_environment()
assert result["available"] is True
assert result["reasons"] == []
assert result["setup"] == []

def test_unavailable_without_fal_key(self):
with patch("tools.image_generation_tool._HAS_FAL_CLIENT", True), \
patch.dict(os.environ, {}, clear=True):
env = {k: v for k, v in os.environ.items() if k != "FAL_KEY"}
with patch.dict(os.environ, env, clear=True):
from tools.image_generation_tool import detect_image_generation_environment
result = detect_image_generation_environment()
assert result["available"] is False
assert any("FAL_KEY" in r for r in result["reasons"])
assert any("fal.ai" in s for s in result["setup"])

def test_unavailable_without_fal_client(self):
with patch("tools.image_generation_tool._HAS_FAL_CLIENT", False), \
patch.dict(os.environ, {"FAL_KEY": "test-key"}):
from tools.image_generation_tool import detect_image_generation_environment
result = detect_image_generation_environment()
assert result["available"] is False
assert any("fal-client" in r for r in result["reasons"])
assert any("pip install fal-client" in s for s in result["setup"])

def test_both_missing_returns_two_reasons(self):
with patch("tools.image_generation_tool._HAS_FAL_CLIENT", False):
env = {k: v for k, v in os.environ.items() if k != "FAL_KEY"}
with patch.dict(os.environ, env, clear=True):
from tools.image_generation_tool import detect_image_generation_environment
result = detect_image_generation_environment()
assert result["available"] is False
assert len(result["reasons"]) == 2
assert len(result["setup"]) == 2


# ---------------------------------------------------------------------------
# _handle_image_generate — fail-fast guard
# ---------------------------------------------------------------------------


class TestHandleImageGenerateFailFast:

def test_returns_structured_error_when_unavailable(self):
unavailable_env = {
"available": False,
"reasons": ["FAL_KEY environment variable is not set"],
"setup": ["Get a free API key at https://fal.ai and set FAL_KEY=<your-key>"],
}
with patch("tools.image_generation_tool.detect_image_generation_environment",
return_value=unavailable_env):
from tools.image_generation_tool import _handle_image_generate
result = json.loads(_handle_image_generate({"prompt": "a cat"}))
assert "error" in result
assert "FAL_KEY" in result["error"]
assert "fal.ai" in result["error"]

def test_error_message_contains_setup_instructions(self):
unavailable_env = {
"available": False,
"reasons": ["fal-client library is not installed", "FAL_KEY environment variable is not set"],
"setup": ["pip install fal-client", "Get a free API key at https://fal.ai and set FAL_KEY=<your-key>"],
}
with patch("tools.image_generation_tool.detect_image_generation_environment",
return_value=unavailable_env):
from tools.image_generation_tool import _handle_image_generate
result = json.loads(_handle_image_generate({"prompt": "a dog"}))
assert "pip install fal-client" in result["error"]
assert "fal.ai" in result["error"]

def test_no_error_when_available_and_prompt_missing(self):
available_env = {"available": True, "reasons": [], "setup": []}
with patch("tools.image_generation_tool.detect_image_generation_environment",
return_value=available_env):
from tools.image_generation_tool import _handle_image_generate
result = json.loads(_handle_image_generate({}))
# Should reach the prompt validation, not the availability guard
assert "error" in result
assert "prompt" in result["error"].lower()


# ---------------------------------------------------------------------------
# check_image_generation_requirements delegates to detect_*
# ---------------------------------------------------------------------------


class TestCheckImageGenerationRequirements:

def test_true_when_env_available(self):
with patch("tools.image_generation_tool.detect_image_generation_environment",
return_value={"available": True, "reasons": [], "setup": []}):
from tools.image_generation_tool import check_image_generation_requirements
assert check_image_generation_requirements() is True

def test_false_when_env_unavailable(self):
with patch("tools.image_generation_tool.detect_image_generation_environment",
return_value={"available": False, "reasons": ["x"], "setup": ["y"]}):
from tools.image_generation_tool import check_image_generation_requirements
assert check_image_generation_requirements() is False
74 changes: 55 additions & 19 deletions tools/image_generation_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,50 @@
)
"""

import importlib.util
import json
import logging
import os
import datetime
from typing import Dict, Any, Optional, Union
import fal_client
from tools.debug_helpers import DebugSession

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Lazy availability probe — checked once per process
# ---------------------------------------------------------------------------

_HAS_FAL_CLIENT = importlib.util.find_spec("fal_client") is not None


def detect_image_generation_environment() -> dict:
"""Probe whether image generation is available in the current environment.

Returns a dict with:
``available`` (bool) — True only when all requirements are met.
``reasons`` (list) — Human-readable strings explaining each gap.
``setup`` (list) — Actionable fix instructions for each gap.
"""
reasons: list[str] = []
setup: list[str] = []

if not _HAS_FAL_CLIENT:
reasons.append("fal-client library is not installed")
setup.append("pip install fal-client")

if not os.getenv("FAL_KEY"):
reasons.append("FAL_KEY environment variable is not set")
setup.append(
"Get a free API key at https://fal.ai and set FAL_KEY=<your-key>"
)

return {
"available": len(reasons) == 0,
"reasons": reasons,
"setup": setup,
}

# Configuration for image generation
DEFAULT_MODEL = "fal-ai/flux-2-pro"
DEFAULT_ASPECT_RATIO = "landscape"
Expand Down Expand Up @@ -186,6 +220,7 @@ def _upscale_image(image_url: str, original_prompt: str) -> Dict[str, Any]:
# The async API (submit_async) caches a global httpx.AsyncClient via
# @cached_property, which breaks when asyncio.run() destroys the loop
# between calls (gateway thread-pool pattern).
import fal_client # noqa: PLC0415 — lazy import, fal_client may not be installed
handler = fal_client.submit(
UPSCALER_MODEL,
arguments=upscaler_arguments
Expand Down Expand Up @@ -312,6 +347,7 @@ def image_generate_tool(
logger.info(" Guidance: %s", validated_params['guidance_scale'])

# Submit request to FAL.ai using sync API (avoids cached event loop issues)
import fal_client # noqa: PLC0415 — lazy import, fal_client may not be installed
handler = fal_client.submit(
DEFAULT_MODEL,
arguments=arguments
Expand Down Expand Up @@ -404,23 +440,8 @@ def check_fal_api_key() -> bool:


def check_image_generation_requirements() -> bool:
"""
Check if all requirements for image generation tools are met.

Returns:
bool: True if requirements are met, False otherwise
"""
try:
# Check API key
if not check_fal_api_key():
return False

# Check if fal_client is available
import fal_client
return True

except ImportError:
return False
"""Return True only when FAL_KEY is set and fal-client is installed."""
return detect_image_generation_environment()["available"]


def get_debug_session_info() -> Dict[str, Any]:
Expand Down Expand Up @@ -536,6 +557,19 @@ def get_debug_session_info() -> Dict[str, Any]:


def _handle_image_generate(args, **kw):
# Fail fast: surface a clear, actionable error instead of silently dropping
# the tool or attempting PIL/web-search workarounds (see issue #2543).
env = detect_image_generation_environment()
if not env["available"]:
msg = (
"Image generation is unavailable in this environment.\n\n"
"Missing requirements:\n"
+ "".join(f" - {r}\n" for r in env["reasons"])
+ "\nTo enable image generation:\n"
+ "".join(f" {i + 1}. {s}\n" for i, s in enumerate(env["setup"]))
)
return json.dumps({"error": msg})

prompt = args.get("prompt", "")
if not prompt:
return json.dumps({"error": "prompt is required for image generation"})
Expand All @@ -555,7 +589,9 @@ def _handle_image_generate(args, **kw):
toolset="image_gen",
schema=IMAGE_GENERATE_SCHEMA,
handler=_handle_image_generate,
check_fn=check_image_generation_requirements,
# check_fn intentionally removed: the handler now returns a structured error
# when backends are unavailable so the model always sees the tool and gets
# a clear setup message rather than silently attempting workarounds.
requires_env=["FAL_KEY"],
is_async=False, # Switched to sync fal_client API to fix "Event loop is closed" in gateway
emoji="🎨",
Expand Down