forked from BerriAI/litellm
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add MiniMax video and image generation providers #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
blackflame007
merged 5 commits into
litellm_internal_staging
from
litellm_minimax_models
Jul 31, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5522a4a
feat: add MiniMax video and image generation providers
blackflame007 ae75297
test(interactions): tolerate Google spec dropping the Content discrim…
blackflame007 f7c62bb
fix: map image_url to MiniMax subject_reference for image-01 characte…
blackflame007 fb628d7
fix: reject MiniMax H3 reference videos so billed input seconds canno…
blackflame007 74facb1
fix(minimax): preserve HTTP errors, prefer MINIMAX_API_KEY, stop alia…
blackflame007 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from collections.abc import Mapping | ||
| from types import MappingProxyType | ||
| from typing import Any # noqa: TID251 # base transformation contracts type these payloads as Any | ||
|
|
||
| import litellm | ||
| from litellm.constants import MINIMAX_MEDIA_DEFAULT_API_BASE | ||
| from litellm.secret_managers.main import get_secret_str | ||
|
|
||
| EMPTY_MAP: Mapping[str, Any] = MappingProxyType({}) # mutable-ok: frozen shared empty mapping | ||
|
|
||
|
|
||
| def resolve_minimax_media_api_base(api_base: str | None) -> str: | ||
| return (api_base or MINIMAX_MEDIA_DEFAULT_API_BASE).rstrip("/") | ||
|
|
||
|
|
||
| def strip_minimax_prefix(model: str) -> str: | ||
| return model.removeprefix("minimax/") | ||
|
|
||
|
|
||
| def drop_none_values(values: Mapping[str, Any]) -> Mapping[str, Any]: | ||
| return {key: value for key, value in values.items() if value is not None} | ||
|
|
||
|
|
||
| def minimax_bearer_headers( | ||
| headers: Mapping[str, Any], | ||
| api_key: str | None, | ||
| ) -> dict: # mutable-ok: validate_environment contracts return dict | ||
| final_api_key = api_key or get_secret_str("MINIMAX_API_KEY") or litellm.api_key | ||
| if not final_api_key: | ||
| raise ValueError("MINIMAX_API_KEY is not set") | ||
| return {**headers, "Authorization": f"Bearer {final_api_key}", "Content-Type": "application/json"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from litellm.types.utils import ImageResponse | ||
|
|
||
|
|
||
| def image_cost_calculator( | ||
| model: str, | ||
| image_response: "ImageResponse", | ||
| ) -> float: | ||
| import litellm | ||
| from litellm.types.utils import ImageResponse as _ImageResponse | ||
|
|
||
| if not isinstance(image_response, _ImageResponse): | ||
| raise ValueError(f"image_response must be of type ImageResponse got type={type(image_response)}") | ||
|
|
||
| model_info = litellm.get_model_info( | ||
| model=model, | ||
| custom_llm_provider=litellm.LlmProviders.MINIMAX.value, | ||
| ) | ||
| output_cost_per_image: float = model_info.get("output_cost_per_image") or 0.0 | ||
| num_images = len(image_response.data) if image_response.data else 0 | ||
| return output_cost_per_image * num_images |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 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: | ||
| return MinimaxImageGenerationConfig() |
229 changes: 229 additions & 0 deletions
229
litellm/llms/minimax/image_generation/transformation.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| from collections.abc import Mapping, Sequence | ||
| from math import gcd | ||
| from types import MappingProxyType | ||
| from typing import TYPE_CHECKING, Any # noqa: TID251 # base transformation contracts type these payloads as Any | ||
|
|
||
| import httpx | ||
|
|
||
| from litellm.llms.base_llm.image_generation.transformation import ( | ||
| BaseImageGenerationConfig, | ||
| ) | ||
| from litellm.llms.minimax.common_utils import ( | ||
| EMPTY_MAP, | ||
| drop_none_values, | ||
| minimax_bearer_headers, | ||
| resolve_minimax_media_api_base, | ||
| strip_minimax_prefix, | ||
| ) | ||
| 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 | ||
|
|
||
| MINIMAX_IMAGE_PASSTHROUGH_PARAMS = frozenset( | ||
| {"aspect_ratio", "width", "height", "seed", "prompt_optimizer", "subject_reference", "image_url"} | ||
| ) | ||
|
|
||
| _V1_ERROR_HTTP_STATUS: Mapping[int, int] = MappingProxyType( | ||
| {1002: 429, 1004: 401, 1008: 402, 2049: 401} # mutable-ok: frozen constant lookup table | ||
| ) | ||
|
|
||
| _SIZE_TO_ASPECT_RATIO: Mapping[str, str] = MappingProxyType( | ||
| { # mutable-ok: frozen constant lookup table | ||
| "1024x1024": "1:1", | ||
| "1280x720": "16:9", | ||
| "1920x1080": "16:9", | ||
| "720x1280": "9:16", | ||
| "1080x1920": "9:16", | ||
| "1152x864": "4:3", | ||
| "864x1152": "3:4", | ||
| "1248x832": "3:2", | ||
| "832x1248": "2:3", | ||
| "1344x576": "21:9", | ||
| } | ||
| ) | ||
|
|
||
| _RESERVED_REQUEST_KEYS = frozenset({"model", "prompt", "user", "size", "extra_body"}) | ||
|
|
||
|
|
||
| def _aspect_ratio_from_size(size: str) -> str: | ||
| mapped = _SIZE_TO_ASPECT_RATIO.get(size) | ||
| if mapped: | ||
| return mapped | ||
| width, _, height = size.partition("x") | ||
| try: | ||
| parsed_width, parsed_height = int(width), int(height) | ||
| except ValueError: | ||
| return size.replace("x", ":") | ||
| if parsed_width <= 0 or parsed_height <= 0: | ||
| return size.replace("x", ":") | ||
| divisor = gcd(parsed_width, parsed_height) | ||
| return f"{parsed_width // divisor}:{parsed_height // divisor}" | ||
|
|
||
|
|
||
| class MinimaxImageGenerationConfig(BaseImageGenerationConfig): | ||
| def get_supported_openai_params( | ||
| self, model: str | ||
| ) -> list[OpenAIImageGenerationOptionalParams]: # mutable-ok: BaseImageGenerationConfig contract returns list | ||
| return ["n", "response_format", "size"] | ||
|
|
||
| def get_complete_url( | ||
| self, | ||
| api_base: str | None, | ||
| api_key: str | None, | ||
| model: str, | ||
| optional_params: Mapping[str, Any], | ||
| litellm_params: Mapping[str, Any], | ||
| stream: bool | None = None, | ||
| ) -> str: | ||
| return f"{resolve_minimax_media_api_base(api_base)}/v1/image_generation" | ||
|
|
||
| def validate_environment( | ||
| self, | ||
| headers: Mapping[str, Any], | ||
| model: str, | ||
| messages: Sequence[AllMessageValues], | ||
| optional_params: Mapping[str, Any], | ||
| litellm_params: Mapping[str, Any], | ||
| api_key: str | None = None, | ||
| api_base: str | None = None, | ||
| ) -> dict: # mutable-ok: BaseImageGenerationConfig contract returns dict | ||
| return minimax_bearer_headers(headers, api_key) | ||
|
|
||
| def map_openai_params( | ||
| self, | ||
| non_default_params: Mapping[str, Any], | ||
| optional_params: Mapping[str, Any], | ||
| model: str, | ||
| drop_params: bool, | ||
| ) -> dict: # mutable-ok: BaseImageGenerationConfig contract returns dict | ||
| supported = self.get_supported_openai_params(model) | ||
| unsupported = tuple( | ||
| key | ||
| for key in non_default_params | ||
| if key != "size" and key not in supported and key not in MINIMAX_IMAGE_PASSTHROUGH_PARAMS | ||
| ) | ||
| if unsupported and not drop_params: | ||
| raise ValueError( | ||
| f"Parameters {list(unsupported)} are not supported for model {model}. Supported " | ||
| f"parameters are {supported}. Set drop_params=True to drop unsupported parameters." | ||
| ) | ||
| size = non_default_params.get("size") | ||
| aspect_ratio = _aspect_ratio_from_size(size) if isinstance(size, str) and size else None | ||
| return { | ||
| **optional_params, | ||
| **{key: value for key, value in non_default_params.items() if key != "size" and key not in unsupported}, | ||
| **({"aspect_ratio": aspect_ratio} if aspect_ratio else EMPTY_MAP), | ||
| } | ||
|
|
||
| def transform_image_generation_request( | ||
| self, | ||
| model: str, | ||
| prompt: str, | ||
| optional_params: Mapping[str, Any], | ||
| litellm_params: Mapping[str, Any], | ||
| headers: Mapping[str, Any], | ||
| ) -> dict: # mutable-ok: BaseImageGenerationConfig contract returns dict | ||
| extra_body = optional_params.get("extra_body") | ||
| params = { | ||
| **optional_params, | ||
| **(extra_body if isinstance(extra_body, dict) else EMPTY_MAP), | ||
| } | ||
| forwarded = drop_none_values( | ||
| {key: value for key, value in params.items() if key not in _RESERVED_REQUEST_KEYS and key != "image_url"} | ||
| ) | ||
| image_url = params.get("image_url") | ||
| reference = ( | ||
| ({"type": "character", "image_file": image_url.strip()},) # mutable-ok: MiniMax wire shape is a JSON object | ||
| if "subject_reference" not in forwarded and isinstance(image_url, str) and image_url.strip() | ||
| else None | ||
| ) | ||
| return dict( | ||
| drop_none_values( | ||
| { | ||
| "model": strip_minimax_prefix(model), | ||
| "prompt": prompt, | ||
| **forwarded, | ||
| "subject_reference": reference if reference is not None else forwarded.get("subject_reference"), | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| def transform_image_generation_response( | ||
| self, | ||
| model: str, | ||
| raw_response: httpx.Response, | ||
| model_response: ImageResponse, | ||
| logging_obj: LiteLLMLoggingObj, | ||
| request_data: Mapping[str, Any], | ||
| optional_params: Mapping[str, Any], | ||
| litellm_params: Mapping[str, Any], | ||
| encoding: Any, | ||
| api_key: str | None = None, | ||
| json_mode: bool | None = None, | ||
| ) -> ImageResponse: | ||
| self._raise_for_status(raw_response) | ||
| response_data = self._parse_json(raw_response) | ||
| self._raise_for_minimax_error(raw_response, response_data) | ||
| data = response_data.get("data") or EMPTY_MAP | ||
| urls = data.get("image_urls") or () | ||
| b64_images = data.get("image_base64") or () | ||
| images = [ | ||
| *(ImageObject(url=url, b64_json=None) for url in urls if isinstance(url, str) and url), | ||
| *(ImageObject(url=None, b64_json=b64) for b64 in b64_images if isinstance(b64, str) and b64), | ||
| ] | ||
| if not images: | ||
| raise ValueError(f"MiniMax image generation returned no images: {response_data}") | ||
| model_response.data = images | ||
| return model_response | ||
|
|
||
| def _raise_for_status(self, raw_response: httpx.Response) -> None: | ||
| if raw_response.is_success: | ||
| return | ||
| raise self.get_error_class( | ||
| error_message=self._error_message_from_body(raw_response), | ||
| status_code=raw_response.status_code, | ||
| headers=raw_response.headers, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _error_message_from_body(raw_response: httpx.Response) -> str: | ||
| try: | ||
| body = raw_response.json() | ||
| except Exception: | ||
| return raw_response.text | ||
| if not isinstance(body, dict): | ||
| return raw_response.text | ||
| error = body.get("error") | ||
| message = error.get("message") if isinstance(error, dict) else None | ||
| if not (isinstance(message, str) and message): | ||
| base_resp = body.get("base_resp") | ||
| message = base_resp.get("status_msg") if isinstance(base_resp, dict) else None | ||
| return message if isinstance(message, str) and message else raw_response.text | ||
|
|
||
| @staticmethod | ||
| def _parse_json(raw_response: httpx.Response) -> Mapping[str, Any]: | ||
| try: | ||
| return raw_response.json() | ||
| except Exception as e: | ||
| raise ValueError(f"Error parsing MiniMax image generation response: {e}") from e | ||
|
|
||
| def _raise_for_minimax_error(self, raw_response: httpx.Response, response_data: Mapping[str, Any]) -> None: | ||
| base_resp = response_data.get("base_resp") or EMPTY_MAP | ||
| status_code = base_resp.get("status_code") | ||
| if status_code in (None, 0): | ||
| return | ||
| message = base_resp.get("status_msg") or "MiniMax API returned an error" | ||
| raise self.get_error_class( | ||
| error_message=f"{message} ({status_code})", | ||
| status_code=_V1_ERROR_HTTP_STATUS.get(status_code, 400), | ||
| headers=raw_response.headers, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .transformation import MinimaxVideoConfig | ||
|
|
||
| __all__ = ("MinimaxVideoConfig",) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the MiniMax endpoint or an intermediary returns a non-2xx response without the v1
base_respenvelope, such as a JSON 401/429 or an HTML 502, the shared image handler does not callraise_for_status()and this transform instead reports a parsing or “no images”ValueError. That discards the real HTTP status, causing incorrect exception mapping and retry behavior; checkraw_response.is_successand raise the provider error with the response status before parsing the success payload.Useful? React with 👍 / 👎.