mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(fal_ai): handle seconds=auto and oversized sizes for minimax h3 videos (#42504)
* test(fal_ai): e2e for minimax h3 auto duration and oversized size Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(fal_ai): keep auto duration literal and make h3 tier lookup total Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(fal_ai): omit duration for h3 when seconds is auto Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fal_ai): move the minimax h3 auto duration and oversized size repro to tests/integration Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
ee3f5a8bdf
commit
5af9b0136c
4 changed files with 87 additions and 1 deletions
|
|
@ -104,7 +104,10 @@ def _profile_for_model(model: str) -> _ModelProfile:
|
|||
|
||||
|
||||
def _resolution_for_short_side(short_side: int, profile: _ModelProfile) -> str:
|
||||
return next(resolution for threshold, resolution in profile.resolution_tiers if short_side <= threshold)
|
||||
return next(
|
||||
(resolution for threshold, resolution in profile.resolution_tiers if short_side <= threshold),
|
||||
profile.resolution_tiers[-1][1],
|
||||
)
|
||||
|
||||
|
||||
def _model_path_from_request_url(raw_response: httpx.Response) -> str | None:
|
||||
|
|
@ -351,6 +354,8 @@ class FalAIVideoConfig(BaseVideoConfig):
|
|||
duration: Final[str | None] = _duration_value(seconds)
|
||||
if duration is None:
|
||||
raise ValueError("fal.ai seconds must be a numeric value")
|
||||
if duration == "auto":
|
||||
return MappingProxyType({}) if profile.integer_duration else MappingProxyType({"duration": duration})
|
||||
return MappingProxyType({"duration": int(duration) if profile.integer_duration else duration})
|
||||
|
||||
def validate_environment(
|
||||
|
|
|
|||
|
|
@ -169,6 +169,12 @@
|
|||
"tests/integration/providers/test_fal_ai_video_wire.py::test_fal_video_failed_result_reports_failed_status_and_fal_error": [
|
||||
"other.provider_wire.fal_ai.video_failed_result_surfaces_fal_error"
|
||||
],
|
||||
"tests/integration/providers/test_fal_ai_video_wire.py::test_fal_h3_auto_duration_omits_duration_and_queues": [
|
||||
"other.provider_wire.fal_ai.h3_auto_duration_omits_duration_and_queues"
|
||||
],
|
||||
"tests/integration/providers/test_fal_ai_video_wire.py::test_fal_h3_oversized_size_uses_top_resolution_tier_and_queues": [
|
||||
"other.provider_wire.fal_ai.h3_oversized_size_uses_top_resolution_tier"
|
||||
],
|
||||
"tests/integration/providers/test_fal_ai_image_wire.py::test_fal_gpt_image_25_generation_sends_quality_and_size_and_charges_keyed_row": [
|
||||
"other.provider_wire.fal_ai.gpt_image_generation_quality_size_wire_and_keyed_pricing"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ from integration._support.wire import Reply, Request, wire_server
|
|||
_MODEL: Final = "bytedance/seedance-2.5/text-to-video"
|
||||
_H3_MODEL: Final = "minimax/h3/text-to-video"
|
||||
_MP4: Final = b"\x00\x00\x00\x18ftypmp42" + uuid.uuid4().bytes * 4
|
||||
_OVERSIZED_SIDE: Final = "9" * 30
|
||||
|
||||
|
||||
def _h3_queue_reply(request_id: str) -> Reply:
|
||||
return Reply(body=json.dumps({"status": "IN_QUEUE", "request_id": request_id, "queue_position": 0}).encode())
|
||||
|
||||
|
||||
@pytest.mark.covers("other.provider_wire.fal_ai.video_queue_create_status_and_content_download")
|
||||
|
|
@ -177,3 +182,61 @@ def test_fal_video_failed_result_reports_failed_status_and_fal_error(gateway: Ga
|
|||
content: Final = gateway.request("GET", f"/v1/videos/{video_id}/content")
|
||||
assert content.status_code == 422, content.text
|
||||
assert "Failed to download the file" in content.text
|
||||
|
||||
|
||||
@pytest.mark.covers("other.provider_wire.fal_ai.h3_auto_duration_omits_duration_and_queues")
|
||||
def test_fal_h3_auto_duration_omits_duration_and_queues(gateway: Gateway) -> None:
|
||||
request_id: Final = "fal-h3-auto-" + uuid.uuid4().hex
|
||||
|
||||
def respond(request: Request) -> Reply:
|
||||
assert request.method == "POST"
|
||||
assert request.target == f"/{_H3_MODEL}"
|
||||
assert json.loads(request.body) == {
|
||||
"prompt": "a cat playing volleyball on a beach",
|
||||
"resolution": "768P",
|
||||
"aspect_ratio": "16:9",
|
||||
}
|
||||
return _h3_queue_reply(request_id)
|
||||
|
||||
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(model=f"fal_ai/{_H3_MODEL}", api_base=wire.url, api_key="synthetic-fal-key")
|
||||
response: Final = gateway.request(
|
||||
"POST",
|
||||
"/v1/videos",
|
||||
{"model": model, "prompt": "a cat playing volleyball on a beach", "seconds": "auto", "size": "1280x720"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["status"] == "queued"
|
||||
assert [(request.method, request.target) for request in wire.drain()] == [("POST", f"/{_H3_MODEL}")]
|
||||
|
||||
|
||||
@pytest.mark.covers("other.provider_wire.fal_ai.h3_oversized_size_uses_top_resolution_tier")
|
||||
def test_fal_h3_oversized_size_uses_top_resolution_tier_and_queues(gateway: Gateway) -> None:
|
||||
request_id: Final = "fal-h3-oversized-" + uuid.uuid4().hex
|
||||
|
||||
def respond(request: Request) -> Reply:
|
||||
assert request.method == "POST"
|
||||
assert request.target == f"/{_H3_MODEL}"
|
||||
assert json.loads(request.body) == {
|
||||
"prompt": "a cat playing volleyball on a beach",
|
||||
"duration": 5,
|
||||
"resolution": "4K",
|
||||
"aspect_ratio": "1:1",
|
||||
}
|
||||
return _h3_queue_reply(request_id)
|
||||
|
||||
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(model=f"fal_ai/{_H3_MODEL}", api_base=wire.url, api_key="synthetic-fal-key")
|
||||
response: Final = gateway.request(
|
||||
"POST",
|
||||
"/v1/videos",
|
||||
{
|
||||
"model": model,
|
||||
"prompt": "a cat playing volleyball on a beach",
|
||||
"seconds": "5",
|
||||
"size": f"{_OVERSIZED_SIDE}x{_OVERSIZED_SIDE}",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["status"] == "queued"
|
||||
assert [(request.method, request.target) for request in wire.drain()] == [("POST", f"/{_H3_MODEL}")]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import sys
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
|
|
@ -86,6 +87,17 @@ class TestFalAIVideoTransformation:
|
|||
assert mapped["reference_image_urls"] == [url]
|
||||
assert "image_url" not in mapped
|
||||
|
||||
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"}
|
||||
|
||||
def test_map_openai_params_h3_size_beyond_tiers_uses_top_resolution(self):
|
||||
side = str(sys.maxsize + 1)
|
||||
assert self.config.map_openai_params({"size": f"{side}x{side}"}, H3_TEXT_MODEL, False) == {
|
||||
"resolution": "4K",
|
||||
"aspect_ratio": "1:1",
|
||||
}
|
||||
|
||||
def test_transform_video_create_request(self):
|
||||
body, files, url = self.config.transform_video_create_request(
|
||||
model=MODEL,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue