mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12: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
7411afa0e7
commit
4e0d872588
@ -19,6 +19,7 @@ from nanobot.config.schema import Base
|
|||||||
from nanobot.providers.image_generation import (
|
from nanobot.providers.image_generation import (
|
||||||
AIHubMixImageGenerationClient,
|
AIHubMixImageGenerationClient,
|
||||||
ImageGenerationError,
|
ImageGenerationError,
|
||||||
|
MiniMaxImageGenerationClient,
|
||||||
OpenRouterImageGenerationClient,
|
OpenRouterImageGenerationClient,
|
||||||
)
|
)
|
||||||
from nanobot.utils.artifacts import (
|
from nanobot.utils.artifacts import (
|
||||||
@ -117,7 +118,9 @@ class ImageGenerationTool(Tool):
|
|||||||
def _provider_config(self) -> ProviderConfig | None:
|
def _provider_config(self) -> ProviderConfig | None:
|
||||||
return self.provider_configs.get(self.config.provider)
|
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()
|
provider = self._provider_config()
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"api_key": provider.api_key if provider else None,
|
"api_key": provider.api_key if provider else None,
|
||||||
@ -129,6 +132,8 @@ class ImageGenerationTool(Tool):
|
|||||||
return OpenRouterImageGenerationClient(**kwargs)
|
return OpenRouterImageGenerationClient(**kwargs)
|
||||||
if self.config.provider == "aihubmix":
|
if self.config.provider == "aihubmix":
|
||||||
return AIHubMixImageGenerationClient(**kwargs)
|
return AIHubMixImageGenerationClient(**kwargs)
|
||||||
|
if self.config.provider == "minimax":
|
||||||
|
return MiniMaxImageGenerationClient(**kwargs)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _missing_api_key_error(self) -> str:
|
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."
|
return "Error: OpenRouter API key is not configured. Set providers.openrouter.apiKey."
|
||||||
if provider == "aihubmix":
|
if provider == "aihubmix":
|
||||||
return "Error: AIHubMix API key is not configured. Set providers.aihubmix.apiKey."
|
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."
|
return f"Error: {provider} API key is not configured."
|
||||||
|
|
||||||
def _resolve_reference_image(self, value: str) -> str:
|
def _resolve_reference_image(self, value: str) -> str:
|
||||||
|
|||||||
@ -642,6 +642,7 @@ def serve(
|
|||||||
image_generation_provider_configs={
|
image_generation_provider_configs={
|
||||||
"openrouter": runtime_config.providers.openrouter,
|
"openrouter": runtime_config.providers.openrouter,
|
||||||
"aihubmix": runtime_config.providers.aihubmix,
|
"aihubmix": runtime_config.providers.aihubmix,
|
||||||
|
"minimax": runtime_config.providers.minimax,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@ -755,6 +756,7 @@ def _run_gateway(
|
|||||||
image_generation_provider_configs={
|
image_generation_provider_configs={
|
||||||
"openrouter": config.providers.openrouter,
|
"openrouter": config.providers.openrouter,
|
||||||
"aihubmix": config.providers.aihubmix,
|
"aihubmix": config.providers.aihubmix,
|
||||||
|
"minimax": config.providers.minimax,
|
||||||
},
|
},
|
||||||
provider_snapshot_loader=load_provider_snapshot,
|
provider_snapshot_loader=load_provider_snapshot,
|
||||||
runtime_model_publisher=lambda model, preset: publish_runtime_model_update(
|
runtime_model_publisher=lambda model, preset: publish_runtime_model_update(
|
||||||
|
|||||||
@ -66,6 +66,7 @@ class Nanobot:
|
|||||||
image_generation_provider_configs={
|
image_generation_provider_configs={
|
||||||
"openrouter": config.providers.openrouter,
|
"openrouter": config.providers.openrouter,
|
||||||
"aihubmix": config.providers.aihubmix,
|
"aihubmix": config.providers.aihubmix,
|
||||||
|
"minimax": config.providers.minimax,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return cls(loop)
|
return cls(loop)
|
||||||
|
|||||||
@ -393,3 +393,136 @@ async def _aihubmix_images_from_payload(
|
|||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
await collect(candidate)
|
await collect(candidate)
|
||||||
return images
|
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