feat(minimax): add image_generation support

This commit is contained in:
octo-patch 2026-08-04 03:44:42 +00:00
parent 41722b1cbc
commit b1e36cb396
7 changed files with 570 additions and 0 deletions

View file

@ -387,6 +387,7 @@ def image_generation(
litellm.LlmProviders.VERTEX_AI,
litellm.LlmProviders.OPENROUTER,
litellm.LlmProviders.DASHSCOPE,
litellm.LlmProviders.MINIMAX,
):
if image_generation_config is None:
raise ValueError(f"image generation config is not supported for {custom_llm_provider}")

View file

@ -0,0 +1,14 @@
"""MiniMax image generation transformation."""
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
from .transformation import MinimaxImageGenerationConfig
__all__ = ["MinimaxImageGenerationConfig", "get_minimax_image_generation_config"]
def get_minimax_image_generation_config(model: str) -> BaseImageGenerationConfig:
"""Get the MiniMax image generation config for the given model."""
return MinimaxImageGenerationConfig()

View file

@ -0,0 +1,261 @@
"""
MiniMax Image Generation Configuration
Maps OpenAI image generation params to the MiniMax image generation API.
API endpoint: POST https://api.minimax.io/v1/image_generation
Request format:
{
"model": "image-01",
"prompt": "<prompt>",
"n": 1,
"aspect_ratio": "1:1",
"response_format": "url"
}
Response format:
{
"data": {"image_urls": ["<url>"], "image_base64": ["<base64>"]},
"metadata": {"success_count": 1, "failed_count": 0},
"base_resp": {"status_code": 0, "status_msg": "success"}
}
Reference: https://platform.minimax.io/docs/api-reference/image-generation-t2i
"""
from typing import TYPE_CHECKING, Any
import httpx
import litellm
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.image_generation.transformation import (
BaseImageGenerationConfig,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
AllMessageValues,
OpenAIImageGenerationOptionalParams,
)
from litellm.types.utils import ImageObject, ImageResponse
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
LiteLLMLoggingObj = _LiteLLMLoggingObj
else:
LiteLLMLoggingObj = Any
DEFAULT_API_BASE = "https://api.minimax.io"
IMAGE_GENERATION_ENDPOINT = "/v1/image_generation"
# OpenAI uses "b64_json", MiniMax uses "base64".
OPENAI_TO_MINIMAX_RESPONSE_FORMAT = {
"b64_json": "base64",
}
class MinimaxImageGenerationException(BaseLLMException):
"""Exception raised for MiniMax image generation API errors."""
def __init__(
self,
status_code: int,
message: str,
headers: dict | httpx.Headers | None = None,
):
super().__init__(status_code=status_code, message=message, headers=headers)
class MinimaxImageGenerationConfig(BaseImageGenerationConfig):
"""
Configuration for MiniMax image generation models (image-01, image-01-live).
"""
def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]:
return ["n", "size", "response_format", "seed", "user", "aspect_ratio"]
def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
"""
Map OpenAI image generation params to MiniMax params.
- `size` (WxH) is expanded to `width` and `height`
- `response_format` "b64_json" is mapped to "base64"
- remaining supported params are passed through
"""
supported_params = self.get_supported_openai_params(model)
for k, v in non_default_params.items():
if k in optional_params:
continue
if k not in supported_params:
continue
if k == "size":
width, height = self._parse_size(v)
if width is not None and height is not None:
optional_params["width"] = width
optional_params["height"] = height
elif k == "response_format":
optional_params["response_format"] = OPENAI_TO_MINIMAX_RESPONSE_FORMAT.get(v, v)
else:
optional_params[k] = v
return optional_params
def get_complete_url(
self,
api_base: str | None,
api_key: str | None,
model: str,
optional_params: dict,
litellm_params: dict,
stream: bool | None = None,
) -> str:
"""
Build the MiniMax image generation endpoint URL.
"""
base_url: str = api_base or get_secret_str("MINIMAX_API_BASE") or DEFAULT_API_BASE
base_url = base_url.rstrip("/")
if base_url.endswith(IMAGE_GENERATION_ENDPOINT):
base_url = base_url[: -len(IMAGE_GENERATION_ENDPOINT)]
if not base_url.endswith("/v1"):
base_url = f"{base_url}/v1"
return f"{base_url}/image_generation"
def validate_environment(
self,
headers: dict,
model: str,
messages: list[AllMessageValues],
optional_params: dict,
litellm_params: dict,
api_key: str | None = None,
api_base: str | None = None,
) -> dict:
"""
Validate the MiniMax environment and set auth headers.
"""
final_api_key: str | None = api_key or get_secret_str("MINIMAX_API_KEY") or litellm.api_key
if not final_api_key:
raise ValueError(
"MiniMax API key is required. Set MINIMAX_API_KEY environment variable or pass api_key parameter."
)
headers["Authorization"] = f"Bearer {final_api_key}"
headers["Content-Type"] = "application/json"
return headers
def transform_image_generation_request(
self,
model: str,
prompt: str,
optional_params: dict,
litellm_params: dict,
headers: dict,
) -> dict:
"""
Build the MiniMax image generation request body.
"""
request_data: dict = {
"model": model,
"prompt": prompt,
}
for k, v in optional_params.items():
if v is None:
continue
if k in {"extra_headers", "extra_body", "user"}:
continue
request_data[k] = v
extra_body = optional_params.get("extra_body")
if isinstance(extra_body, dict):
request_data.update({k: v for k, v in extra_body.items() if v is not None})
return request_data
def transform_image_generation_response(
self,
model: str,
raw_response: httpx.Response,
model_response: ImageResponse,
logging_obj: LiteLLMLoggingObj,
request_data: dict,
optional_params: dict,
litellm_params: dict,
encoding: Any,
api_key: str | None = None,
json_mode: bool | None = None,
) -> ImageResponse:
"""
Transform the MiniMax response into a litellm ImageResponse.
MiniMax returns images under `data.image_urls` (response_format=url) or
`data.image_base64` (response_format=base64).
"""
try:
response_data = raw_response.json()
except Exception as e:
raise self.get_error_class(
error_message=f"Failed to parse MiniMax image generation response: {e}",
status_code=raw_response.status_code,
headers=raw_response.headers,
)
base_resp = response_data.get("base_resp") or {}
status_code = base_resp.get("status_code")
if status_code not in (None, 0, "0"):
raise self.get_error_class(
error_message=str(base_resp.get("status_msg") or response_data),
status_code=raw_response.status_code,
headers=raw_response.headers,
)
if not model_response.data:
model_response.data = []
data = response_data.get("data") or {}
for image_url in data.get("image_urls") or []:
model_response.data.append(ImageObject(url=image_url))
for image_base64 in data.get("image_base64") or []:
model_response.data.append(ImageObject(b64_json=image_base64))
return model_response
def get_error_class(
self,
error_message: str,
status_code: int,
headers: dict | httpx.Headers,
) -> BaseLLMException:
return MinimaxImageGenerationException(
status_code=status_code,
message=error_message,
headers=headers,
)
@staticmethod
def _parse_size(size: Any) -> tuple[int | None, int | None]:
"""
Parse an OpenAI `WxH` size string into width/height integers.
MiniMax accepts width/height in [512, 2048] divisible by 8.
"""
if not isinstance(size, str) or "x" not in size:
return None, None
parts = size.split("x")
if len(parts) != 2:
return None, None
try:
width, height = int(parts[0]), int(parts[1])
except ValueError:
return None, None
if width < 512 or width > 2048 or height < 512 or height > 2048:
return None, None
if width % 8 != 0 or height % 8 != 0:
return None, None
return width, height

View file

@ -8662,6 +8662,12 @@ class ProviderConfigManager:
)
return get_dashscope_image_generation_config(model)
elif LlmProviders.MINIMAX == provider:
from litellm.llms.minimax.image_generation import (
get_minimax_image_generation_config,
)
return get_minimax_image_generation_config(model)
elif LlmProviders.MODELSCOPE == provider:
from litellm.llms.modelscope.image_generation import (
get_modelscope_image_generation_config,

View file

@ -27266,6 +27266,22 @@
"max_input_tokens": 1000000,
"max_output_tokens": 128000
},
"minimax/image-01": {
"litellm_provider": "minimax",
"mode": "image_generation",
"source": "https://platform.minimax.io/docs/api-reference/image-generation-t2i",
"supported_endpoints": [
"/v1/image_generation"
]
},
"minimax/image-01-live": {
"litellm_provider": "minimax",
"mode": "image_generation",
"source": "https://platform.minimax.io/docs/api-reference/image-generation-t2i",
"supported_endpoints": [
"/v1/image_generation"
]
},
"mistral.devstral-2-123b": {
"input_cost_per_token": 4e-07,
"litellm_provider": "bedrock_converse",

View file

@ -0,0 +1 @@
# MiniMax image generation tests

View file

@ -0,0 +1,271 @@
"""
Unit tests for the MiniMax image generation configuration.
These tests validate the MinimaxImageGenerationConfig class which handles
transformation between OpenAI-compatible image generation params and the
MiniMax image generation API (POST /v1/image_generation).
"""
import json
from unittest.mock import MagicMock, patch
import httpx
import pytest
from litellm.llms.minimax.image_generation.transformation import (
MinimaxImageGenerationConfig,
)
from litellm.types.utils import ImageResponse
class TestMinimaxImageGenerationTransformation:
def setup_method(self):
self.config = MinimaxImageGenerationConfig()
self.model = "image-01"
self.logging_obj = MagicMock()
def test_get_supported_openai_params(self):
supported_params = self.config.get_supported_openai_params(self.model)
assert "n" in supported_params
assert "size" in supported_params
assert "response_format" in supported_params
assert "seed" in supported_params
assert "aspect_ratio" in supported_params
def test_map_openai_params_passthrough(self):
non_default_params = {
"n": 2,
"seed": 42,
"aspect_ratio": "16:9",
}
result = self.config.map_openai_params(
non_default_params=non_default_params,
optional_params={},
model=self.model,
drop_params=False,
)
assert result["n"] == 2
assert result["seed"] == 42
assert result["aspect_ratio"] == "16:9"
def test_map_openai_params_size_to_width_height(self):
result = self.config.map_openai_params(
non_default_params={"size": "1024x1024"},
optional_params={},
model=self.model,
drop_params=False,
)
assert result["width"] == 1024
assert result["height"] == 1024
assert "size" not in result
def test_map_openai_params_unsupported_size_is_dropped(self):
result = self.config.map_openai_params(
non_default_params={"size": "100x50"},
optional_params={},
model=self.model,
drop_params=False,
)
assert "width" not in result
assert "height" not in result
def test_map_openai_params_response_format_b64_json(self):
result = self.config.map_openai_params(
non_default_params={"response_format": "b64_json"},
optional_params={},
model=self.model,
drop_params=False,
)
assert result["response_format"] == "base64"
def test_map_openai_params_response_format_url(self):
result = self.config.map_openai_params(
non_default_params={"response_format": "url"},
optional_params={},
model=self.model,
drop_params=False,
)
assert result["response_format"] == "url"
def test_get_complete_url_default(self):
result = self.config.get_complete_url(
api_base=None,
api_key="test_key",
model=self.model,
optional_params={},
litellm_params={},
)
assert result == "https://api.minimax.io/v1/image_generation"
def test_get_complete_url_with_custom_base(self):
result = self.config.get_complete_url(
api_base="https://api.minimaxi.com",
api_key="test_key",
model=self.model,
optional_params={},
litellm_params={},
)
assert result == "https://api.minimaxi.com/v1/image_generation"
def test_get_complete_url_with_full_endpoint_base(self):
result = self.config.get_complete_url(
api_base="https://api.minimax.io/v1/image_generation",
api_key="test_key",
model=self.model,
optional_params={},
litellm_params={},
)
assert result == "https://api.minimax.io/v1/image_generation"
@patch("litellm.llms.minimax.image_generation.transformation.get_secret_str")
def test_validate_environment(self, mock_get_secret):
mock_get_secret.return_value = "test_api_key"
headers = {}
result = self.config.validate_environment(
headers=headers,
model=self.model,
messages=[],
optional_params={},
litellm_params={},
api_key=None,
)
assert result["Authorization"] == "Bearer test_api_key"
assert result["Content-Type"] == "application/json"
@patch("litellm.llms.minimax.image_generation.transformation.get_secret_str")
def test_validate_environment_missing_api_key(self, mock_get_secret):
mock_get_secret.return_value = None
with pytest.raises(ValueError):
self.config.validate_environment(
headers={},
model=self.model,
messages=[],
optional_params={},
litellm_params={},
api_key=None,
)
def test_transform_image_generation_request(self):
optional_params = {
"n": 2,
"response_format": "url",
"prompt_optimizer": True,
}
request_data = self.config.transform_image_generation_request(
model=self.model,
prompt="a red apple",
optional_params=optional_params,
litellm_params={},
headers={},
)
assert request_data["model"] == "image-01"
assert request_data["prompt"] == "a red apple"
assert request_data["n"] == 2
assert request_data["response_format"] == "url"
assert request_data["prompt_optimizer"] is True
def test_transform_image_generation_request_merges_extra_body(self):
optional_params = {
"extra_body": {"seed": 7, "prompt_optimizer": True},
}
request_data = self.config.transform_image_generation_request(
model=self.model,
prompt="a red apple",
optional_params=optional_params,
litellm_params={},
headers={},
)
assert request_data["seed"] == 7
assert request_data["prompt_optimizer"] is True
def test_transform_image_generation_response_urls(self):
raw_response = self._make_response(
{
"data": {"image_urls": ["https://example.com/a.png", "https://example.com/b.png"]},
"metadata": {"success_count": 2, "failed_count": 0},
"base_resp": {"status_code": 0, "status_msg": "success"},
}
)
model_response = self.config.transform_image_generation_response(
model=self.model,
raw_response=raw_response,
model_response=ImageResponse(),
logging_obj=self.logging_obj,
request_data={},
optional_params={},
litellm_params={},
encoding=None,
)
assert len(model_response.data) == 2
assert model_response.data[0].url == "https://example.com/a.png"
assert model_response.data[1].url == "https://example.com/b.png"
def test_transform_image_generation_response_base64(self):
raw_response = self._make_response(
{
"data": {"image_base64": ["aGVsbG8=", "d29ybGQ="]},
"metadata": {"success_count": 2, "failed_count": 0},
"base_resp": {"status_code": 0, "status_msg": "success"},
}
)
model_response = self.config.transform_image_generation_response(
model=self.model,
raw_response=raw_response,
model_response=ImageResponse(),
logging_obj=self.logging_obj,
request_data={},
optional_params={},
litellm_params={},
encoding=None,
)
assert len(model_response.data) == 2
assert model_response.data[0].b64_json == "aGVsbG8="
assert model_response.data[1].b64_json == "d29ybGQ="
def test_transform_image_generation_response_error_status_code(self):
raw_response = self._make_response(
{
"base_resp": {"status_code": 1004, "status_msg": "invalid api key"},
}
)
with pytest.raises(Exception):
self.config.transform_image_generation_response(
model=self.model,
raw_response=raw_response,
model_response=ImageResponse(),
logging_obj=self.logging_obj,
request_data={},
optional_params={},
litellm_params={},
encoding=None,
)
@staticmethod
def _make_response(payload: dict) -> httpx.Response:
return httpx.Response(
status_code=200,
json=payload,
request=httpx.Request("POST", "https://api.minimax.io/v1/image_generation"),
)