Skip to content
Open
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
80 changes: 79 additions & 1 deletion src/backend/bisheng/database/data/t_gpts_tools.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,82 @@
[
{
"name": "MiniMax Image Generation",
"logo": null,
"desc": "Generate images from a text prompt with MiniMax",
"api_params": [
{
"in": "query",
"name": "prompt",
"schema": {"type": "string"},
"required": true,
"description": "Text description of the image"
},
{
"in": "query",
"name": "model",
"schema": {"type": "string", "enum": ["image-01", "image-01-live"], "default": "image-01"},
"required": false,
"description": "MiniMax image model"
},
{
"in": "query",
"name": "aspect_ratio",
"schema": {"type": "string"},
"required": false,
"description": "Generated image aspect ratio"
},
{
"in": "query",
"name": "width",
"schema": {"type": "integer", "minimum": 512, "maximum": 2048, "multipleOf": 8},
"required": false,
"description": "Generated image width"
},
{
"in": "query",
"name": "height",
"schema": {"type": "integer", "minimum": 512, "maximum": 2048, "multipleOf": 8},
"required": false,
"description": "Generated image height"
},
{
"in": "query",
"name": "response_format",
"schema": {"type": "string", "enum": ["url", "base64"], "default": "url"},
"required": false,
"description": "Image response format"
},
{
"in": "query",
"name": "seed",
"schema": {"type": "integer"},
"required": false,
"description": "Seed for deterministic generation"
},
{
"in": "query",
"name": "n",
"schema": {"type": "integer", "minimum": 1, "maximum": 9, "default": 1},
"required": false,
"description": "Number of images to generate"
},
{
"in": "query",
"name": "prompt_optimizer",
"schema": {"type": "boolean", "default": false},
"required": false,
"description": "Enable prompt optimization"
}
],
"tool_key": "minimax_image_generation",
"type": 17,
"is_preset": 1,
"is_delete": 0,
"user_id": 1,
"create_time": "2026-08-12 00:00:00",
"update_time": "2026-08-12 00:00:00",
"id": 39
},
{
"name": "时间",
"logo": null,
Expand Down Expand Up @@ -1115,4 +1193,4 @@
"user_id": 1,
"id": 38
}
]
]
16 changes: 16 additions & 0 deletions src/backend/bisheng/database/data/t_gpts_tools_type.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
[
{
"id": 17,
"name": "MiniMax",
"logo": "",
"description": "Generate images from text prompts with MiniMax image models",
"server_host": "",
"auth_method": 0,
"api_key": "",
"auth_type": "basic",
"is_preset": 1,
"user_id": null,
"is_delete": 0,
"create_time": "2026-08-12 00:00:00",
"update_time": "2026-08-12 00:00:00",
"openapi_schema": null
},
{
"id": 1,
"name": "时间",
Expand Down
2 changes: 2 additions & 0 deletions src/backend/bisheng/utils/mask_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(self):
'api_secret': self.mask_api_key,
'openai_api_key': self.mask_api_key,
'siliconflow_api_key': self.mask_api_key,
'minimax_api_key': self.mask_api_key,
'email_password': self.mask_api_key,
'jina_api_key': self.mask_api_key,
'app_secret': self.mask_api_key,
Expand All @@ -42,6 +43,7 @@ def __init__(self):
'api_secret': r'^\*+$',
'openai_api_key': r'^\*+$',
'siliconflow_api_key': r'^\*+$',
'minimax_api_key': r'^\*+$',
'email_password': r'^\*+$',
'jina_api_key': r'^\*+$',
'app_secret': r'^\*+$',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# from .eastmoney import
from bisheng_langchain.gpts.tools.api_tools.firecrawl import FireCrawl
from bisheng_langchain.gpts.tools.api_tools.jina import JinaTool
from bisheng_langchain.gpts.tools.api_tools.minimax_image import MiniMaxImage
from bisheng_langchain.gpts.tools.api_tools.silicon_flow import SiliconFlow
from bisheng_langchain.gpts.tools.message.dingding import DingdingMessageTool
from bisheng_langchain.gpts.tools.message.email import EmailMessageTool
Expand Down Expand Up @@ -74,6 +75,9 @@
'silicon_stable_diffusion': (SiliconFlow.get_api_tool, ['siliconflow_api_key']),
'silicon_flux': (SiliconFlow.get_api_tool, ['siliconflow_api_key'])
}
_MINIMAX_IMAGE_TOOLS: Dict[str, Tuple[Callable[[KwArg(Any)], BaseTool], List[str]]] = {
"minimax_image_generation": (MiniMaxImage.get_api_tool, ["minimax_api_key", "minimax_base_url"])
}
dingding_class_methods = [
method for method in DingdingMessageTool.__dict__
if isinstance(DingdingMessageTool.__dict__[method], classmethod)
Expand Down Expand Up @@ -120,6 +124,7 @@
ALL_API_TOOLS.update(_FIRE_TOOLS)
ALL_API_TOOLS.update(_JINA_TOOLS)
ALL_API_TOOLS.update(_SILICON_TOOLS)
ALL_API_TOOLS.update(_MINIMAX_IMAGE_TOOLS)
ALL_API_TOOLS.update(_DING_TOOLS)
ALL_API_TOOLS.update(_EMAIL_TOOLS)
ALL_API_TOOLS.update(_FEISHU_TOOLS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import json
from typing import Any, Literal

import requests
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field, model_validator

from bisheng_langchain.gpts.tools.api_tools.base import APIToolBase, MultArgsSchemaTool
from bisheng_langchain.gpts.tools.api_tools.minimax_image_core import (
build_image_generation_payload,
parse_image_generation_response,
)


class MiniMaxImageInput(BaseModel):
prompt: str = Field(description="Text description of the image", min_length=1, max_length=1500)
model: Literal["image-01", "image-01-live"] = "image-01"
aspect_ratio: Literal["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"] | None = None
width: int | None = Field(default=None, ge=512, le=2048, multiple_of=8)
height: int | None = Field(default=None, ge=512, le=2048, multiple_of=8)
response_format: Literal["url", "base64"] = "url"
seed: int | None = None
n: int = Field(default=1, ge=1, le=9)
prompt_optimizer: bool = False

@model_validator(mode="after")
def validate_dimensions(self) -> "MiniMaxImageInput":
if (self.width is None) != (self.height is None):
raise ValueError("width and height must be provided together")
return self


class MiniMaxImage(APIToolBase):
minimax_api_key: str = Field(description="MiniMax API key")
minimax_base_url: str = Field(default="https://api.minimax.io", description="MiniMax API base URL")

def generate(
self,
prompt: str,
model: str = "image-01",
aspect_ratio: str | None = None,
width: int | None = None,
height: int | None = None,
response_format: str = "url",
seed: int | None = None,
n: int = 1,
prompt_optimizer: bool = False,
) -> str:
"""Generate images from a text prompt with MiniMax."""
payload = build_image_generation_payload(
prompt=prompt,
model=model,
aspect_ratio=aspect_ratio,
width=width,
height=height,
response_format=response_format,
seed=seed,
n=n,
prompt_optimizer=prompt_optimizer,
)
response = requests.post(
f"{self.minimax_base_url.rstrip('/')}/v1/image_generation",
json=payload,
headers={"Authorization": f"Bearer {self.minimax_api_key}", "Content-Type": "application/json"},
timeout=self.request_timeout,
)
response.raise_for_status()
result = parse_image_generation_response(response.json(), response_format)
return json.dumps(result, ensure_ascii=False)

@classmethod
def get_api_tool(cls, name: str, **kwargs: Any) -> BaseTool:
instance = cls(**kwargs)
return MultArgsSchemaTool(
name=name,
description=instance.generate.__doc__,
func=instance.generate,
args_schema=MiniMaxImageInput,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Any


def build_image_generation_payload(
*,
prompt: str,
model: str,
aspect_ratio: str | None,
width: int | None,
height: int | None,
response_format: str,
seed: int | None,
n: int,
prompt_optimizer: bool,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"model": model,
"prompt": prompt,
"response_format": response_format,
"n": n,
"prompt_optimizer": prompt_optimizer,
}
optional_fields = {
"aspect_ratio": aspect_ratio,
"width": width,
"height": height,
"seed": seed,
}
payload.update({key: value for key, value in optional_fields.items() if value is not None})
return payload


def parse_image_generation_response(payload: dict[str, Any], response_format: str) -> dict[str, Any]:
base_response = payload.get("base_resp")
if not isinstance(base_response, dict) or base_response.get("status_code") != 0:
message = base_response.get("status_msg") if isinstance(base_response, dict) else None
raise ValueError(message or "MiniMax image generation failed")

data = payload.get("data")
if not isinstance(data, dict):
raise ValueError("MiniMax image generation returned no image data")
response_key = "image_urls" if response_format == "url" else "image_base64"
images = data.get(response_key)
if not isinstance(images, list) or not images or not all(isinstance(image, str) for image in images):
raise ValueError("MiniMax image generation returned invalid image data")

metadata = payload.get("metadata")
metadata = metadata if isinstance(metadata, dict) else {}
return {
"response_format": response_format,
"images": images,
"success_count": metadata.get("success_count", len(images)),
"failed_count": metadata.get("failed_count", 0),
}
76 changes: 76 additions & 0 deletions src/backend/test/tool/test_minimax_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import importlib.util
import unittest
from pathlib import Path

MODULE_PATH = Path(__file__).parents[2] / "bisheng_langchain" / "gpts" / "tools" / "api_tools" / "minimax_image_core.py"
SPEC = importlib.util.spec_from_file_location("minimax_image_core", MODULE_PATH)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
build_image_generation_payload = MODULE.build_image_generation_payload
parse_image_generation_response = MODULE.parse_image_generation_response


class TestMiniMaxImagePayload(unittest.TestCase):
def test_builds_supported_request_fields(self):
payload = build_image_generation_payload(
prompt="A small robot reading a book",
model="image-01-live",
aspect_ratio=None,
width=1024,
height=768,
response_format="base64",
seed=7,
n=2,
prompt_optimizer=True,
)

self.assertEqual(
payload,
{
"model": "image-01-live",
"prompt": "A small robot reading a book",
"width": 1024,
"height": 768,
"response_format": "base64",
"seed": 7,
"n": 2,
"prompt_optimizer": True,
},
)

def test_parses_url_response_and_metadata(self):
result = parse_image_generation_response(
{
"data": {"image_urls": ["https://example.test/image.png"]},
"metadata": {"success_count": 1, "failed_count": 0},
"base_resp": {"status_code": 0, "status_msg": "success"},
},
"url",
)

self.assertEqual(result["images"], ["https://example.test/image.png"])
self.assertEqual(result["success_count"], 1)
self.assertEqual(result["failed_count"], 0)

def test_parses_base64_response(self):
result = parse_image_generation_response(
{
"data": {"image_base64": ["aW1hZ2U="]},
"base_resp": {"status_code": 0, "status_msg": "success"},
},
"base64",
)

self.assertEqual(result["images"], ["aW1hZ2U="])

def test_rejects_provider_error(self):
with self.assertRaisesRegex(ValueError, "invalid request"):
parse_image_generation_response(
{"base_resp": {"status_code": 2013, "status_msg": "invalid request"}},
"url",
)


if __name__ == "__main__":
unittest.main()
4 changes: 3 additions & 1 deletion src/frontend/platform/public/locales/en-US/tool.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@
"fieldCannotBeEmpty": "{{label}} cannot be empty",
"jinaApiKeyLabel": "Jina API Key",
"siliconflowApiKeyLabel": "SiliconFlow API Key",
"minimaxApiKeyLabel": "MiniMax API Key",
"minimaxApiBaseUrlLabel": "MiniMax API Base URL",
"uploadFile": "Upload File",
"organizeKnowledgeBase": "Organize Knowledge Base",
"个人知识库": "Personal Knowledge Base",
Expand All @@ -406,4 +408,4 @@
"写入文件内容desc": "Append text to a file or create one if it does not exist.",
"替换文件指定行范围内容": "Replace File Line Range",
"替换文件指定行范围内容desc": "Replace a specified range of lines in a file."
}
}
4 changes: 3 additions & 1 deletion src/frontend/platform/public/locales/ja/tool.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@
"fieldCannotBeEmpty": "{{label}}は空にできません",
"jinaApiKeyLabel": "Jina API Key",
"siliconflowApiKeyLabel": "SiliconFlow API Key",
"minimaxApiKeyLabel": "MiniMax API Key",
"minimaxApiBaseUrlLabel": "MiniMax API Base URL",
"uploadFile": "ファイルをアップロード",
"organizeKnowledgeBase": "ナレッジベース整理",
"个人知识库": "個人ナレッジベース",
Expand All @@ -406,4 +408,4 @@
"写入文件内容desc": "テキストを追記し、存在しない場合は新規作成します。",
"替换文件指定行范围内容": "行範囲を置換",
"替换文件指定行范围内容desc": "ファイル内の指定行範囲を置き換えます。"
}
}
Loading