fix(fal_ai): keep model in polled video ids and pick resolution from the short side

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kerry 2026-09-19 17:01:09 +00:00
parent 141548dcf3
commit c359ef763e
2 changed files with 58 additions and 34 deletions

View file

@ -79,12 +79,22 @@ def _resolution_for_short_side(short_side: int) -> str:
return next((resolution for threshold, resolution in _RESOLUTION_TIERS if short_side <= threshold), "4k")
def _model_path_from_queue_url(url: object) -> str | None:
if not isinstance(url, str) or not url:
def _model_path_from_request_url(raw_response: httpx.Response) -> str | None:
segments: Final[tuple[str, ...]] = tuple(segment for segment in raw_response.request.url.path.split("/") if segment)
if "requests" not in segments:
return None
path: Final[str] = httpx.URL(url).path.strip("/")
model_path, separator, _ = path.partition("/requests/")
return model_path if separator and model_path else None
model_segments: Final[tuple[str, ...]] = segments[: segments.index("requests")]
segment_count: Final[int] = 3 if len(model_segments) >= 3 and model_segments[-3] in _QUEUE_NAMESPACES else 2
return "/".join(model_segments[-segment_count:]) if len(model_segments) >= segment_count else None
def _request_id_from_request_url(raw_response: httpx.Response) -> str | None:
segments: Final[tuple[str, ...]] = tuple(segment for segment in raw_response.request.url.path.split("/") if segment)
if "requests" not in segments:
return None
request_index: Final[int] = segments.index("requests")
request_id_index: Final[int] = request_index + 1
return segments[request_id_index] if len(segments) > request_id_index else None
def _size_params(size: object) -> Mapping[str, str]:
@ -295,12 +305,16 @@ class FalAIVideoConfig(BaseVideoConfig):
error_value: Final[object] = response_data.get("error")
error: Final[str | None] = error_value if isinstance(error_value, str) else None
provider: Final[str] = custom_llm_provider or _FAL_AI_PROVIDER
model_path: Final[str | None] = _model_path_from_queue_url(response_data.get("response_url"))
model_path: Final[str | None] = _model_path_from_request_url(raw_response)
request_id: Final[str] = _response_string(response_data, "request_id") or (
_request_id_from_request_url(raw_response) or ""
)
return VideoObject(
id=encode_video_id_with_provider(_response_string(response_data, "request_id"), provider, model_path),
id=encode_video_id_with_provider(request_id, provider, model_path),
object="video",
status="failed" if error else status,
created_at=0,
model=model_path,
error=(
{"code": "fal_error", "message": error} if error else None # mutable-ok: VideoObject requires a dict
),

View file

@ -161,8 +161,8 @@ class TestFalAIVideoTransformation:
],
)
def test_status_response_mapping(self, response_data, expected_status):
response = Mock(spec=httpx.Response)
response.json.return_value = response_data
status_url = "https://queue.fal.run/bytedance/seedance-2.5/requests/abc/status"
response = httpx.Response(200, json=response_data, request=httpx.Request("GET", status_url))
video = self.config.transform_video_status_retrieve_response(
raw_response=response,
@ -172,43 +172,32 @@ class TestFalAIVideoTransformation:
assert video.status == expected_status
assert video.created_at == 0
decoded = decode_video_id_with_provider(video.id)
assert decoded["model_id"] == "bytedance/seedance-2.5"
assert decoded["video_id"] == "abc"
def test_status_response_id_stays_pollable(self):
response = Mock(spec=httpx.Response)
response.json.return_value = {
"request_id": "abc",
"status": "IN_PROGRESS",
"response_url": "https://queue.fal.run/bytedance/seedance-2.5/requests/abc",
}
video = self.config.transform_video_status_retrieve_response(
raw_response=response,
logging_obj=self.logging_obj,
custom_llm_provider="fal_ai",
)
status_url, _ = self.config.transform_video_status_retrieve_request(
poll_url, _ = self.config.transform_video_status_retrieve_request(
video_id=video.id,
api_base="https://queue.fal.run",
litellm_params=GenericLiteLLMParams(),
headers={},
)
content_url, _ = self.config.transform_video_content_request(
video_id=video.id,
api_base="https://queue.fal.run",
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert status_url == "https://queue.fal.run/bytedance/seedance-2.5/requests/abc/status"
assert content_url == "https://queue.fal.run/bytedance/seedance-2.5/requests/abc"
assert poll_url == status_url
def test_status_response_error(self):
response = Mock(spec=httpx.Response)
response.json.return_value = {
response_data = {
"request_id": "abc",
"status": "COMPLETED",
"error": "generation failed",
}
response = httpx.Response(
200,
json=response_data,
request=httpx.Request(
"GET",
"https://queue.fal.run/bytedance/seedance-2.5/requests/abc/status",
),
)
video = self.config.transform_video_status_retrieve_response(
raw_response=response,
@ -219,6 +208,27 @@ class TestFalAIVideoTransformation:
assert video.status == "failed"
assert video.error == {"code": "fal_error", "message": "generation failed"}
def test_status_response_uses_namespaced_request_url(self):
response = httpx.Response(
200,
json={"status": "IN_PROGRESS"},
request=httpx.Request(
"GET",
"https://example.com/proxy/workflows/owner/app/requests/xyz/status",
),
)
video = self.config.transform_video_status_retrieve_response(
raw_response=response,
logging_obj=self.logging_obj,
custom_llm_provider="fal_ai",
)
decoded = decode_video_id_with_provider(video.id)
assert decoded["model_id"] == "workflows/owner/app"
assert decoded["video_id"] == "xyz"
assert video.model == "workflows/owner/app"
def test_content_response_downloads_video_url(self, monkeypatch):
content_response = httpx.Response(
200,