mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 08:02:30 +00:00
feat: add MiniMax image generation provider support
Add MiniMaxImageGenerationClient with support for: - Text-to-image generation via MiniMax image-01 model - Reference image support (subject_reference) - Aspect ratio selection - Proper error handling aligned with existing providers Wire up MiniMax provider config in ImageGenerationTool, gateway, serve, and Nanobot class.
This commit is contained in:
parent
d4ade8f680
commit
557f4e6ae9
@ -19,6 +19,7 @@ from nanobot.config.schema import Base
|
||||
from nanobot.providers.image_generation import (
|
||||
AIHubMixImageGenerationClient,
|
||||
ImageGenerationError,
|
||||
MiniMaxImageGenerationClient,
|
||||
OpenRouterImageGenerationClient,
|
||||
)
|
||||
from nanobot.utils.artifacts import (
|
||||
@ -117,7 +118,9 @@ class ImageGenerationTool(Tool):
|
||||
def _provider_config(self) -> ProviderConfig | None:
|
||||
return self.provider_configs.get(self.config.provider)
|
||||
|
||||
def _provider_client(self) -> OpenRouterImageGenerationClient | AIHubMixImageGenerationClient | None:
|
||||
def _provider_client(
|
||||
self,
|
||||
) -> OpenRouterImageGenerationClient | AIHubMixImageGenerationClient | MiniMaxImageGenerationClient | None:
|
||||
provider = self._provider_config()
|
||||
kwargs = {
|
||||
"api_key": provider.api_key if provider else None,
|
||||
@ -129,6 +132,8 @@ class ImageGenerationTool(Tool):
|
||||
return OpenRouterImageGenerationClient(**kwargs)
|
||||
if self.config.provider == "aihubmix":
|
||||
return AIHubMixImageGenerationClient(**kwargs)
|
||||
if self.config.provider == "minimax":
|
||||
return MiniMaxImageGenerationClient(**kwargs)
|
||||
return None
|
||||
|
||||
def _missing_api_key_error(self) -> str:
|
||||
@ -137,6 +142,8 @@ class ImageGenerationTool(Tool):
|
||||
return "Error: OpenRouter API key is not configured. Set providers.openrouter.apiKey."
|
||||
if provider == "aihubmix":
|
||||
return "Error: AIHubMix API key is not configured. Set providers.aihubmix.apiKey."
|
||||
if provider == "minimax":
|
||||
return "Error: MiniMax API key is not configured. Set providers.minimax.apiKey."
|
||||
return f"Error: {provider} API key is not configured."
|
||||
|
||||
def _resolve_reference_image(self, value: str) -> str:
|
||||
|
||||
@ -642,6 +642,7 @@ def serve(
|
||||
image_generation_provider_configs={
|
||||
"openrouter": runtime_config.providers.openrouter,
|
||||
"aihubmix": runtime_config.providers.aihubmix,
|
||||
"minimax": runtime_config.providers.minimax,
|
||||
},
|
||||
)
|
||||
except ValueError as exc:
|
||||
@ -755,6 +756,7 @@ def _run_gateway(
|
||||
image_generation_provider_configs={
|
||||
"openrouter": config.providers.openrouter,
|
||||
"aihubmix": config.providers.aihubmix,
|
||||
"minimax": config.providers.minimax,
|
||||
},
|
||||
provider_snapshot_loader=load_provider_snapshot,
|
||||
runtime_model_publisher=lambda model, preset: publish_runtime_model_update(
|
||||
|
||||
@ -66,6 +66,7 @@ class Nanobot:
|
||||
image_generation_provider_configs={
|
||||
"openrouter": config.providers.openrouter,
|
||||
"aihubmix": config.providers.aihubmix,
|
||||
"minimax": config.providers.minimax,
|
||||
},
|
||||
)
|
||||
return cls(loop)
|
||||
|
||||
@ -393,3 +393,136 @@ async def _aihubmix_images_from_payload(
|
||||
for candidate in candidates:
|
||||
await collect(candidate)
|
||||
return images
|
||||
|
||||
|
||||
_MINIMAX_TIMEOUT_S = 300.0
|
||||
|
||||
_MINIMAX_ASPECT_RATIO_SIZES = {
|
||||
"1:1": "1:1",
|
||||
"16:9": "16:9",
|
||||
"4:3": "4:3",
|
||||
"3:2": "3:2",
|
||||
"2:3": "2:3",
|
||||
"3:4": "3:4",
|
||||
"9:16": "9:16",
|
||||
"21:9": "21:9",
|
||||
}
|
||||
|
||||
|
||||
class MiniMaxImageGenerationClient:
|
||||
"""Async client for MiniMax image generation API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None,
|
||||
api_base: str | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
timeout: float = _MINIMAX_TIMEOUT_S,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
) -> None:
|
||||
self.api_key = api_key
|
||||
self.api_base = _provider_base_url(
|
||||
"minimax",
|
||||
api_base,
|
||||
"https://api.minimaxi.com/v1",
|
||||
)
|
||||
self.extra_headers = extra_headers or {}
|
||||
self.extra_body = extra_body or {}
|
||||
self.timeout = timeout
|
||||
self._client = client
|
||||
|
||||
def _resolve_aspect_ratio(self, aspect_ratio: str | None) -> str:
|
||||
if aspect_ratio and aspect_ratio in _MINIMAX_ASPECT_RATIO_SIZES:
|
||||
return _MINIMAX_ASPECT_RATIO_SIZES[aspect_ratio]
|
||||
return "1:1"
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
*,
|
||||
prompt: str,
|
||||
model: str,
|
||||
reference_images: list[str] | None = None,
|
||||
aspect_ratio: str | None = None,
|
||||
image_size: str | None = None,
|
||||
) -> GeneratedImageResponse:
|
||||
if not self.api_key:
|
||||
raise ImageGenerationError(
|
||||
"MiniMax API key is not configured. Set providers.minimax.apiKey."
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
**self.extra_headers,
|
||||
}
|
||||
|
||||
body: dict[str, Any] = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"response_format": "base64",
|
||||
}
|
||||
|
||||
resolved_ratio = self._resolve_aspect_ratio(aspect_ratio)
|
||||
body["aspect_ratio"] = resolved_ratio
|
||||
|
||||
refs = list(reference_images or [])
|
||||
if refs:
|
||||
image_refs = [image_path_to_data_url(path) for path in refs]
|
||||
body["subject_reference"] = [
|
||||
{"type": "character", "image_file": ref} for ref in image_refs
|
||||
]
|
||||
|
||||
body.update(self.extra_body)
|
||||
|
||||
if self._client is not None:
|
||||
return await self._generate_with_client(self._client, body, headers)
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
return await self._generate_with_client(client, body, headers)
|
||||
|
||||
async def _generate_with_client(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
body: dict[str, Any],
|
||||
headers: dict[str, str],
|
||||
) -> GeneratedImageResponse:
|
||||
url = f"{self.api_base}/image_generation"
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=body)
|
||||
except httpx.TimeoutException as exc:
|
||||
raise ImageGenerationError("MiniMax image generation timed out") from exc
|
||||
except httpx.RequestError as exc:
|
||||
raise ImageGenerationError(f"MiniMax image generation request failed: {exc}") from exc
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
detail = response.text[:500]
|
||||
raise ImageGenerationError(f"MiniMax image generation failed: {detail}") from exc
|
||||
|
||||
payload = response.json()
|
||||
images = _minimax_images_from_payload(payload)
|
||||
|
||||
if not images:
|
||||
provider_error = payload.get("error") if isinstance(payload, dict) else None
|
||||
if provider_error:
|
||||
raise ImageGenerationError(f"MiniMax returned no images: {provider_error}")
|
||||
raise ImageGenerationError("MiniMax returned no images for this request")
|
||||
|
||||
return GeneratedImageResponse(images=images, content="", raw=payload)
|
||||
|
||||
|
||||
def _minimax_images_from_payload(payload: dict[str, Any]) -> list[str]:
|
||||
"""Extract base64 images from MiniMax API response.
|
||||
|
||||
MiniMax returns images in ``data.image_base64`` (list of base64 strings).
|
||||
"""
|
||||
images: list[str] = []
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, dict):
|
||||
return images
|
||||
for b64 in data.get("image_base64") or []:
|
||||
if isinstance(b64, str) and b64:
|
||||
images.append(_b64_png_data_url(b64))
|
||||
return images
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user