From 7c7e02b06fa9fbf74d1e3c5064e5921e62aeb51a Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Wed, 11 Nov 2026 21:05:41 -0800 Subject: [PATCH] fix(fal_ai): give MiniMax H3 Max video endpoints their own resolution profile (#51348) * fix(fal_ai): give MiniMax H3 Max video endpoints their own resolution profile Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fal_ai): keep the H3 Max test signature on one line Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fal_ai): type the H3 Max test locals and signatures Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/fal_ai/videos/transformation.py | 16 ++++++- .../test_fal_ai_video_transformation.py | 45 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/litellm/llms/fal_ai/videos/transformation.py b/litellm/llms/fal_ai/videos/transformation.py index c658dab4d3f..9974f72c025 100644 --- a/litellm/llms/fal_ai/videos/transformation.py +++ b/litellm/llms/fal_ai/videos/transformation.py @@ -45,7 +45,7 @@ _ALLOWED_ASPECT_RATIOS: Final[frozenset[str]] = frozenset({"auto", "16:9", "9:16 class _ModelProfile: resolutions: frozenset[str] resolution_tiers: tuple[tuple[int, str], ...] - default_resolution: str + default_resolution: str | None integer_duration: bool reference_key: str reference_as_list: bool @@ -67,6 +67,14 @@ _H3_PROFILE: Final[_ModelProfile] = _ModelProfile( reference_key="reference_image_urls", reference_as_list=True, ) +_H3_MAX_PROFILE: Final[_ModelProfile] = _ModelProfile( + resolutions=frozenset({"480P", "768P", "1080P"}), + resolution_tiers=((480, "480P"), (768, "768P"), (sys.maxsize, "1080P")), + default_resolution=None, + integer_duration=False, + reference_key="image_url", + reference_as_list=False, +) _QUEUE_NAMESPACES: Final[frozenset[str]] = frozenset(("workflows", "comfy")) _STATUS_MAP: Final[Mapping[str, str]] = MappingProxyType( { @@ -101,7 +109,11 @@ def _duration_value(value: object) -> str | None: def _profile_for_model(model: str) -> _ModelProfile: - return _H3_PROFILE if model.startswith("minimax/h3/") else _SEEDANCE_PROFILE + if model.startswith("minimax/h3/"): + return _H3_PROFILE + if model.startswith("minimax/h3-max"): + return _H3_MAX_PROFILE + return _SEEDANCE_PROFILE def _resolution_for_short_side(short_side: int, profile: _ModelProfile) -> str: diff --git a/tests/unit/llms/fal_ai/videos/test_fal_ai_video_transformation.py b/tests/unit/llms/fal_ai/videos/test_fal_ai_video_transformation.py index 2505d635375..8f7c794b157 100644 --- a/tests/unit/llms/fal_ai/videos/test_fal_ai_video_transformation.py +++ b/tests/unit/llms/fal_ai/videos/test_fal_ai_video_transformation.py @@ -23,6 +23,9 @@ from litellm.utils import ProviderConfigManager MODEL = "bytedance/seedance-2.5/text-to-video" H3_TEXT_MODEL = "minimax/h3/text-to-video" H3_REFERENCE_MODEL = "minimax/h3/reference-to-video" +H3_MAX_TEXT_MODEL = "minimax/h3-max/text-to-video" +H3_MAX_TURBO_IMAGE_MODEL = "minimax/h3-max-turbo/image-to-video" +H3_MAX_RECAST_MODEL = "minimax/h3-max/recast" @pytest.mark.parametrize("drop_params", [False, True]) @@ -96,6 +99,24 @@ class TestFalAIVideoTransformation: assert mapped["reference_image_urls"] == [url] assert "image_url" not in mapped + def test_map_openai_params_supports_h3_max_profile(self) -> None: + url: Final = "https://example.com/image.png" + + assert self.config.map_openai_params({"size": "768p"}, H3_MAX_TEXT_MODEL, False) == {"resolution": "768P"} + assert self.config.map_openai_params({"size": "1920x1080"}, H3_MAX_TEXT_MODEL, False) == { + "resolution": "1080P", + "aspect_ratio": "16:9", + } + mapped: Final = self.config.map_openai_params( + {"seconds": 5.5, "input_reference": url, "size": "1280x768"}, + H3_MAX_TURBO_IMAGE_MODEL, + False, + ) + assert mapped["resolution"] == "768P" + assert mapped["image_url"] == url + assert "reference_image_urls" not in mapped + assert mapped["duration"] == "5" + def test_map_openai_params_h3_omits_auto_duration(self): assert self.config.map_openai_params({"seconds": "auto"}, H3_TEXT_MODEL, False) == {} assert self.config.map_openai_params({"seconds": "auto"}, MODEL, False) == {"duration": "auto"} @@ -198,6 +219,30 @@ class TestFalAIVideoTransformation: assert video.usage == {"duration_seconds": 5.0, "video_resolution": "2K"} + def test_transform_video_create_response_h3_max_leaves_default_resolution_to_cost_map( + self, local_model_cost_map: None + ) -> None: + response: Final = Mock(spec=httpx.Response) + response.json.return_value = {"request_id": "abc"} + + video: Final = self.config.transform_video_create_response( + model=H3_MAX_RECAST_MODEL, + raw_response=response, + logging_obj=self.logging_obj, + custom_llm_provider="fal_ai", + request_data={"duration": 5}, + ) + + assert video.usage == {"duration_seconds": 5.0} + row: Final = litellm.model_cost[f"fal_ai/{H3_MAX_RECAST_MODEL}"] + assert default_video_cost_calculator( + model=H3_MAX_RECAST_MODEL, + duration_seconds=5.0, + custom_llm_provider="fal_ai", + model_info=None, + video_resolution=None, + ) == pytest.approx(row["output_cost_per_second"] * 5.0) + def test_status_request_uses_queue_base_path(self): response = Mock(spec=httpx.Response) response.json.return_value = {"request_id": "abc"}