From 91808ed2cf53dbeed76ed63908cea8b9cbcaf24f Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Thu, 12 Nov 2026 15:34:24 -0800 Subject: [PATCH] fix(fal_ai): bill Luma Ray v3.2 videos at fal's 540p default resolution (#51477) fal's luma/agent/ray/v3.2 endpoints default resolution to 540p, but the fal video transformation had no Luma profile and fell back to the Seedance profile, whose default is 720p. A request that left resolution unset was stamped video_resolution 720p and priced from output_cost_per_second_720p, which is twice the 540p rate on text-to-video, image-to-video and reframe and 1.5x on video-to-video. A Luma Ray profile (540p, 720p, 1080p, default 540p) stamps the real default, so those requests price from output_cost_per_second, the 540p rate. Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/fal_ai/videos/transformation.py | 10 ++++++++ .../test_fal_ai_video_transformation.py | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/litellm/llms/fal_ai/videos/transformation.py b/litellm/llms/fal_ai/videos/transformation.py index 838e7065ef7..d6cd7f0d5ce 100644 --- a/litellm/llms/fal_ai/videos/transformation.py +++ b/litellm/llms/fal_ai/videos/transformation.py @@ -75,6 +75,14 @@ _H3_MAX_PROFILE: Final[_ModelProfile] = _ModelProfile( reference_key="image_url", reference_as_list=False, ) +_LUMA_RAY_PROFILE: Final[_ModelProfile] = _ModelProfile( + resolutions=frozenset({"540p", "720p", "1080p"}), + resolution_tiers=((540, "540p"), (720, "720p"), (sys.maxsize, "1080p")), + default_resolution="540p", + integer_duration=False, + reference_key="image_url", + reference_as_list=False, +) _SEEDANCE_REFERENCE_PROFILE: Final[_ModelProfile] = replace( _SEEDANCE_PROFILE, reference_key="image_urls", @@ -118,6 +126,8 @@ def _profile_for_model(model: str) -> _ModelProfile: return _H3_PROFILE if model.startswith("minimax/h3-max"): return _H3_MAX_PROFILE + if model.startswith("luma/agent/ray/"): + return _LUMA_RAY_PROFILE is_seedance_reference: Final = model.startswith("bytedance/seedance-") and model.endswith("/reference-to-video") return _SEEDANCE_REFERENCE_PROFILE if is_seedance_reference else _SEEDANCE_PROFILE 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 b20c8069789..3498663775f 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 @@ -232,6 +232,29 @@ class TestFalAIVideoTransformation: assert video.usage == {"duration_seconds": 5.0, "video_resolution": "2K"} + def test_transform_video_create_response_luma_ray_defaults_to_540p(self): + response = Mock(spec=httpx.Response) + response.json.return_value = {"request_id": "abc"} + + for model in ( + "luma/agent/ray/v3.2/text-to-video", + "luma/agent/ray/v3.2/image-to-video", + "luma/agent/ray/v3.2/reframe", + "luma/agent/ray/v3.2/video-to-video", + ): + video = self.config.transform_video_create_response( + model=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, "video_resolution": "540p"}, model + row = litellm.model_cost[f"fal_ai/{model}"] + assert default_video_cost_calculator(model, 5, "fal_ai", video_resolution="540p") == ( + 5 * row["output_cost_per_second"] + ), model + def test_transform_video_create_response_h3_max_leaves_default_resolution_to_cost_map( self, local_model_cost_map: None ) -> None: