diff --git a/.env.example b/.env.example index 76be6ce26d2f..90531388986a 100644 --- a/.env.example +++ b/.env.example @@ -128,6 +128,10 @@ # Get at: https://fal.ai/ # FAL_KEY= +# Minimax API Key - High-quality image generation with various styles +# Get at: https://www.minimax.io/ +# MINIMAX_API_KEY= + # Honcho - Cross-session AI-native user modeling (optional) # Builds a persistent understanding of the user across sessions and tools. # Get at: https://app.honcho.dev diff --git a/acp_adapter/tools.py b/acp_adapter/tools.py index 52313220b7f6..b570763e5d56 100644 --- a/acp_adapter/tools.py +++ b/acp_adapter/tools.py @@ -44,6 +44,7 @@ "delegate_task": "execute", "vision_analyze": "read", "image_generate": "execute", + "minimax_image_generate": "execute", "text_to_speech": "execute", # Thinking / meta "_thinking": "think", diff --git a/agent/display.py b/agent/display.py index 063b7bb1c7ce..fef5fc81c39a 100644 --- a/agent/display.py +++ b/agent/display.py @@ -182,7 +182,7 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int | None = None) - "read_file": "path", "write_file": "path", "patch": "path", "search_files": "pattern", "browser_navigate": "url", "browser_click": "ref", "browser_type": "text", - "image_generate": "prompt", "text_to_speech": "text", + "image_generate": "prompt", "minimax_image_generate": "prompt", "text_to_speech": "text", "vision_analyze": "question", "mixture_of_agents": "user_prompt", "skill_view": "name", "skills_list": "category", "cronjob": "action", @@ -910,6 +910,8 @@ def _wrap(line: str) -> str: return _wrap(f"┊ 📚 skill {_trunc(args.get('name', ''), 30)} {dur}") if tool_name == "image_generate": return _wrap(f"┊ 🎨 create {_trunc(args.get('prompt', ''), 35)} {dur}") + if tool_name == "minimax_image_generate": + return _wrap(f"┊ 🎨 create {_trunc(args.get('prompt', ''), 35)} {dur}") if tool_name == "text_to_speech": return _wrap(f"┊ 🔊 speak {_trunc(args.get('text', ''), 30)} {dur}") if tool_name == "vision_analyze": diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index c61d6995b6df..8b991697018d 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -832,6 +832,7 @@ def build_nous_subscription_prompt(valid_tool_names: "set[str] | None" = None) - "browser_get_images", "browser_vision", "image_generate", + "minimax_image_generate", "text_to_speech", "terminal", "process", diff --git a/hermes_cli/config.py b/hermes_cli/config.py index d06338aa14e4..e692a5cc973c 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -847,6 +847,7 @@ def _ensure_hermes_home_managed(home: Path): "description": "MiniMax API key (international)", "prompt": "MiniMax API key", "url": "https://www.minimax.io/", + "tools": ["minimax_image_generate"], "password": True, "category": "provider", "advanced": True, diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 5fe8cdc79ee9..b9e7cb757d9f 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -242,6 +242,14 @@ def _get_plugin_toolset_keys() -> set: "managed_nous_feature": "image_gen", "override_env_vars": ["FAL_KEY"], }, + { + "name": "MiniMax", + "badge": "paid", + "tag": "High-quality image generation with a variety of styles", + "env_vars": [ + {"key": "MINIMAX_API_KEY", "prompt": "MiniMax API key", "url": "https://platform.minimax.io/user-center/payment/token-plan"}, + ], + }, { "name": "FAL.ai", "badge": "paid", @@ -814,7 +822,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 get_env_value("FAL_KEY") and not get_env_value("MINIMAX_API_KEY") return not _toolset_has_keys(ts_key, config) diff --git a/model_tools.py b/model_tools.py index 801255b79780..3380fcfd5d24 100644 --- a/model_tools.py +++ b/model_tools.py @@ -168,7 +168,7 @@ def _run_async(coro): "terminal_tools": ["terminal"], "vision_tools": ["vision_analyze"], "moa_tools": ["mixture_of_agents"], - "image_tools": ["image_generate"], + "image_tools": ["image_generate", "minimax_image_generate"], "skills_tools": ["skills_list", "skill_view", "skill_manage"], "browser_tools": [ "browser_navigate", "browser_snapshot", "browser_click", diff --git a/tools/minimax_image_tool.py b/tools/minimax_image_tool.py new file mode 100644 index 000000000000..b5709ed2ae15 --- /dev/null +++ b/tools/minimax_image_tool.py @@ -0,0 +1,211 @@ +""" +MiniMax Image Generation Tool + +Generates images from text prompts using MiniMax's image-01 model via their REST API. +Returns a base64-decoded image saved to a temp file and served as a URL, or the raw +base64 data depending on response_format. + +Requires: MINIMAX_API_KEY environment variable. +""" + +import base64 +import datetime +import json +import logging +import os +import tempfile +import uuid + +import requests + +from tools.debug_helpers import DebugSession + +logger = logging.getLogger(__name__) + +MINIMAX_IMAGE_API_URL = "https://api.minimax.io/v1/image_generation" +MINIMAX_IMAGE_MODEL = "image-01" + +# Aspect ratio mapping — simplified choices for model to select +MINIMAX_ASPECT_RATIO_MAP = { + "landscape": "16:9", + "square": "1:1", + "portrait": "9:16", +} + +_debug = DebugSession("minimax_image_tools", env_var="MINIMAX_IMAGE_DEBUG") + + +def _get_minimax_api_key() -> str | None: + """Return the MiniMax API key from environment variables.""" + return os.getenv("MINIMAX_API_KEY") + + +def minimax_image_generate_tool( + prompt: str, + aspect_ratio: str = "landscape", +) -> str: + """ + Generate an image from a text prompt using MiniMax image-01. + + Args: + prompt: The text prompt describing the desired image. + aspect_ratio: "landscape", "square", or "portrait". + + Returns: + JSON string with {"success": bool, "image": str|None}. + """ + debug_call_data = { + "parameters": {"prompt": prompt, "aspect_ratio": aspect_ratio}, + "error": None, + "success": False, + "generation_time": 0, + } + + start_time = datetime.datetime.now() + + try: + if not prompt or not isinstance(prompt, str) or not prompt.strip(): + raise ValueError("Prompt is required and must be a non-empty string") + + api_key = _get_minimax_api_key() + if not api_key: + raise ValueError("MINIMAX_API_KEY environment variable not set") + + # Resolve aspect ratio + ar_lower = aspect_ratio.lower().strip() if aspect_ratio else "landscape" + if ar_lower not in MINIMAX_ASPECT_RATIO_MAP: + logger.warning("Invalid aspect_ratio '%s', defaulting to 'landscape'", aspect_ratio) + ar_lower = "landscape" + resolved_ar = MINIMAX_ASPECT_RATIO_MAP[ar_lower] + + logger.info("Generating image with MiniMax image-01: %s", prompt[:80]) + + payload = { + "model": MINIMAX_IMAGE_MODEL, + "prompt": prompt.strip(), + "aspect_ratio": resolved_ar, + "response_format": "url", + } + + headers = {"Authorization": f"Bearer {api_key}"} + + resp = requests.post( + MINIMAX_IMAGE_API_URL, + headers=headers, + json=payload, + timeout=120, + ) + resp.raise_for_status() + data = resp.json() + + generation_time = (datetime.datetime.now() - start_time).total_seconds() + + # Handle url response format + if "data" in data and "image_urls" in data["data"]: + urls = data["data"]["image_urls"] + if not urls: + raise ValueError("No image URLs returned from MiniMax API") + image_url = urls[0] + elif "data" in data and "image_base64" in data["data"]: + # Fallback: if server returns base64 despite requesting url + images_b64 = data["data"]["image_base64"] + if not images_b64: + raise ValueError("No images returned from MiniMax API") + # Write to temp file and return a file:// path + raw = base64.b64decode(images_b64[0]) + tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + tmp.write(raw) + tmp.close() + image_url = f"file://{tmp.name}" + else: + raise ValueError(f"Unexpected response structure from MiniMax API: {list(data.keys())}") + + logger.info("MiniMax image generated in %.1fs", generation_time) + + response_data = {"success": True, "image": image_url} + + debug_call_data["success"] = True + debug_call_data["generation_time"] = generation_time + _debug.log_call("minimax_image_generate", debug_call_data) + _debug.save() + + return json.dumps(response_data, indent=2, ensure_ascii=False) + + except Exception as e: + generation_time = (datetime.datetime.now() - start_time).total_seconds() + error_msg = f"Error generating image via MiniMax: {e}" + logger.error("%s", error_msg, exc_info=True) + + response_data = { + "success": False, + "image": None, + "error": str(e), + "error_type": type(e).__name__, + } + + debug_call_data["error"] = error_msg + debug_call_data["generation_time"] = generation_time + _debug.log_call("minimax_image_generate", debug_call_data) + _debug.save() + + return json.dumps(response_data, indent=2, ensure_ascii=False) + + +def check_minimax_image_requirements() -> bool: + """Check if MiniMax image generation requirements are met.""" + return bool(_get_minimax_api_key()) + + +# --------------------------------------------------------------------------- +# Registry +# --------------------------------------------------------------------------- +from tools.registry import registry, tool_error # noqa: E402 + +MINIMAX_IMAGE_GENERATE_SCHEMA = { + "name": "minimax_image_generate", + "description": ( + "Generate high-quality images from text prompts using MiniMax image-01 model. " + "Returns a single image URL. Display it using markdown: ![description](URL)" + ), + "parameters": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The text prompt describing the desired image. Be detailed and descriptive.", + }, + "aspect_ratio": { + "type": "string", + "enum": ["landscape", "square", "portrait"], + "description": ( + "The aspect ratio of the generated image. " + "'landscape' is 16:9 wide, 'portrait' is 9:16 tall, 'square' is 1:1." + ), + "default": "landscape", + }, + }, + "required": ["prompt"], + }, +} + + +def _handle_minimax_image_generate(args, **kw): + prompt = args.get("prompt", "") + if not prompt: + return tool_error("prompt is required for image generation") + return minimax_image_generate_tool( + prompt=prompt, + aspect_ratio=args.get("aspect_ratio", "landscape"), + ) + + +registry.register( + name="minimax_image_generate", + toolset="image_gen", + schema=MINIMAX_IMAGE_GENERATE_SCHEMA, + handler=_handle_minimax_image_generate, + check_fn=check_minimax_image_requirements, + requires_env=["MINIMAX_API_KEY"], + is_async=False, + emoji="🎨", +) diff --git a/toolsets.py b/toolsets.py index 09ee8de09be1..0e554431458f 100644 --- a/toolsets.py +++ b/toolsets.py @@ -36,7 +36,7 @@ # File manipulation "read_file", "write_file", "patch", "search_files", # Vision + image generation - "vision_analyze", "image_generate", + "vision_analyze", "image_generate", "minimax_image_generate", # Skills "skills_list", "skill_view", "skill_manage", # Browser automation @@ -87,7 +87,7 @@ "image_gen": { "description": "Creative generation tools (images)", - "tools": ["image_generate"], + "tools": ["image_generate", "minimax_image_generate"], "includes": [] }, @@ -252,7 +252,7 @@ # File manipulation "read_file", "write_file", "patch", "search_files", # Vision + image generation - "vision_analyze", "image_generate", + "vision_analyze", "image_generate", "minimax_image_generate", # Skills "skills_list", "skill_view", "skill_manage", # Browser automation