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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions acp_adapter/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion agent/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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":
Expand Down
1 change: 1 addition & 0 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
211 changes: 211 additions & 0 deletions tools/minimax_image_tool.py
Original file line number Diff line number Diff line change
@@ -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="🎨",
)
6 changes: 3 additions & 3 deletions toolsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -87,7 +87,7 @@

"image_gen": {
"description": "Creative generation tools (images)",
"tools": ["image_generate"],
"tools": ["image_generate", "minimax_image_generate"],
"includes": []
},

Expand Down Expand Up @@ -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
Expand Down