From b767b7cbf468ad7288a4b169e19013d51b582f0a Mon Sep 17 00:00:00 2001 From: yuwk <1729065730@qq.com> Date: Fri, 11 Sep 2026 13:34:24 +0800 Subject: [PATCH] fix(bedrock): infer video content type from url extension A remote video url served with a missing or generic content type (binary/octet-stream) could not be resolved to a Bedrock video format, so the request raised before it was built. The extension map only covered image and document formats Add the nine video extensions Converse accepts (mp4, mov, mkv, webm, flv, mpeg, mpg, wmv, 3gp) to that map, so a remote clip resolves to a video block the same way a data uri already does Verified without network by composing the real helpers: a .mp4 url with a binary/octet-stream header now yields video/mp4, Bedrock format mp4 and a video block --- .../prompt_templates/common_utils.py | 9 +++++ .../prompt_templates/factory.py | 7 ---- ...ore_utils_prompt_templates_common_utils.py | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 2485896184e..47211067c77 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1565,6 +1565,15 @@ def infer_content_type_from_url_and_content( "png": "image/png", "gif": "image/gif", "webp": "image/webp", + "mp4": "video/mp4", + "mov": "video/mov", + "mkv": "video/mkv", + "webm": "video/webm", + "flv": "video/flv", + "mpeg": "video/mpeg", + "mpg": "video/mpg", + "wmv": "video/wmv", + "3gp": "video/3gp", # Document formats "pdf": "application/pdf", "csv": "text/csv", diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 67d78c0cb8f..493f19faf5b 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -4372,9 +4372,6 @@ class BedrockConverseMessagesProcessor: ) _parts.append(_part) elif element["type"] == "video_url": - # see the sync path: video shares the image - # processor, which keys the block type off the - # mime type. video_element = element["video_url"] if isinstance(video_element, dict): video_url = video_element["url"] @@ -4760,10 +4757,6 @@ def _bedrock_converse_messages_pt( ) _parts.append(_part) elif element["type"] == "video_url": - # OpenAI `video_url` parts must reach Converse as - # video blocks too. `process_image_sync` picks the - # block type from the mime type (video/* -> video, - # image/* -> image), so a video shares this path. video_element = element["video_url"] if isinstance(video_element, dict): video_url = video_element["url"] diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py index b5890d1a5b0..b3b5b08e8da 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py @@ -19,6 +19,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( get_format_from_file_id, handle_any_messages_to_chat_completion_str_messages_conversion, hoist_images_from_tool_messages, + infer_content_type_from_url_and_content, is_encrypted_reasoning_block, responses_reasoning_items_from_thinking_blocks, split_concatenated_json_objects, @@ -1813,3 +1814,35 @@ class TestEncryptedReasoningReplay: strip_encrypted_reasoning_from_messages(messages) assert messages == before + + +@pytest.mark.parametrize( + "url,expected", + [ + ("https://example.com/clip.mp4", "video/mp4"), + ("https://example.com/clip.webm?X-Amz-Signature=abc123", "video/webm"), + ("https://example.com/clip.mov", "video/mov"), + ("https://example.com/clip.mkv", "video/mkv"), + ("https://example.com/clip.3gp", "video/3gp"), + ], +) +def test_infer_video_content_type_from_url_extension(url, expected): + assert ( + infer_content_type_from_url_and_content( + url=url, + content=b"\x00\x00\x00\x18ftypmp42", + current_content_type="binary/octet-stream", + ) + == expected + ) + + +def test_infer_video_content_type_from_url_extension_without_header(): + assert ( + infer_content_type_from_url_and_content( + url="https://example.com/clip.mp4", + content=b"\x00\x00\x00\x18ftypmp42", + current_content_type=None, + ) + == "video/mp4" + )