Skip to content

Commit 5d18d18

Browse files
committed
feat(bedrock): S3 URL pass-through for multimodal content blocks
1 parent b96f033 commit 5d18d18

File tree

8 files changed

+917
-92
lines changed

8 files changed

+917
-92
lines changed

litellm/litellm_core_utils/prompt_templates/factory.py

Lines changed: 161 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hashlib
44
import json
55
import mimetypes
6+
import os
67
import re
78
import xml.etree.ElementTree as ET
89
from enum import Enum
@@ -33,6 +34,7 @@
3334
ChatCompletionToolCallFunctionChunk,
3435
ChatCompletionToolMessage,
3536
ChatCompletionUserMessage,
37+
ChatCompletionVideoObject,
3638
OpenAIMessageContentListBlock,
3739
)
3840
from litellm.types.llms.vertex_ai import FunctionCall as VertexFunctionCall
@@ -3446,6 +3448,7 @@ def stringify_json_tool_call_content(messages: List) -> List:
34463448
from litellm.types.llms.bedrock import ContentBlock as BedrockContentBlock
34473449
from litellm.types.llms.bedrock import DocumentBlock as BedrockDocumentBlock
34483450
from litellm.types.llms.bedrock import ImageBlock as BedrockImageBlock
3451+
from litellm.types.llms.bedrock import S3Location as BedrockS3Location
34493452
from litellm.types.llms.bedrock import SourceBlock as BedrockSourceBlock
34503453
from litellm.types.llms.bedrock import ToolBlock as BedrockToolBlock
34513454
from litellm.types.llms.bedrock import (
@@ -3459,6 +3462,7 @@ def stringify_json_tool_call_content(messages: List) -> List:
34593462
from litellm.types.llms.bedrock import ToolSpecBlock as BedrockToolSpecBlock
34603463
from litellm.types.llms.bedrock import ToolUseBlock as BedrockToolUseBlock
34613464
from litellm.types.llms.bedrock import VideoBlock as BedrockVideoBlock
3465+
from litellm.utils import supports_s3_input
34623466

34633467

34643468
def _parse_content_type(content_type: str) -> str:
@@ -3476,7 +3480,14 @@ def _parse_mime_type(base64_data: str) -> Optional[str]:
34763480

34773481

34783482
class BedrockImageProcessor:
3479-
"""Handles both sync and async image processing for Bedrock conversations."""
3483+
"""Handles both sync and async image/media processing for Bedrock conversations."""
3484+
3485+
@staticmethod
3486+
def _extract_video_url(element: Union[dict, "ChatCompletionVideoObject"]) -> str:
3487+
"""Extract the URL string from a video_url content element."""
3488+
if isinstance(element["video_url"], dict):
3489+
return element["video_url"]["url"]
3490+
return element["video_url"]
34803491

34813492
@staticmethod
34823493
def _post_call_image_processing(
@@ -3695,20 +3706,98 @@ def _create_bedrock_block(
36953706
image=BedrockImageBlock(source=_blob, format=image_format)
36963707
)
36973708

3709+
@staticmethod
3710+
def _get_bedrock_format_from_extension(extension: str) -> str:
3711+
"""Map file extension to Bedrock format enum value."""
3712+
# 3gpp is an alias for 3gp
3713+
if extension == "3gpp":
3714+
return "3gp"
3715+
# jpg → jpeg
3716+
if extension == "jpg":
3717+
return "jpeg"
3718+
return extension
3719+
3720+
@classmethod
3721+
def _create_s3_bedrock_block(
3722+
cls, s3_url: str, format: Optional[str] = None
3723+
) -> BedrockContentBlock:
3724+
"""
3725+
Create a Bedrock content block referencing an S3 object.
3726+
Determines block type (image/document/video) from file extension,
3727+
or from the explicit ``format`` override if provided.
3728+
"""
3729+
# If explicit format override provided (e.g. from file.format field),
3730+
# use it directly instead of requiring a file extension.
3731+
if format:
3732+
raw_format = format.split("/")[-1] if "/" in format else format
3733+
bedrock_format = cls._get_bedrock_format_from_extension(raw_format.lower())
3734+
else:
3735+
extension = os.path.splitext(s3_url)[-1].lstrip(".").lower()
3736+
if not extension:
3737+
raise ValueError(
3738+
f"Cannot determine file type from S3 URL (no extension): {s3_url}"
3739+
)
3740+
bedrock_format = cls._get_bedrock_format_from_extension(extension)
3741+
3742+
s3_source = BedrockSourceBlock(s3Location=BedrockS3Location(uri=s3_url))
3743+
3744+
config = litellm.AmazonConverseConfig()
3745+
supported_image_formats = config.get_supported_image_types()
3746+
supported_doc_formats = config.get_supported_document_types()
3747+
supported_video_formats = config.get_supported_video_types()
3748+
3749+
if bedrock_format in supported_image_formats:
3750+
return BedrockContentBlock(
3751+
image=BedrockImageBlock(source=s3_source, format=bedrock_format)
3752+
)
3753+
elif bedrock_format in supported_doc_formats:
3754+
doc_name = f"s3doc_{hashlib.sha256(s3_url.encode()).hexdigest()[:16]}_{bedrock_format}"
3755+
return BedrockContentBlock(
3756+
document=BedrockDocumentBlock(
3757+
source=s3_source, format=bedrock_format, name=doc_name
3758+
)
3759+
)
3760+
elif bedrock_format in supported_video_formats:
3761+
return BedrockContentBlock(
3762+
video=BedrockVideoBlock(source=s3_source, format=bedrock_format)
3763+
)
3764+
else:
3765+
raise ValueError(
3766+
f"Unsupported file format '{bedrock_format}' for Bedrock S3 content. "
3767+
f"Supported: images={supported_image_formats}, "
3768+
f"documents={supported_doc_formats}, "
3769+
f"videos={supported_video_formats}"
3770+
)
3771+
3772+
# TODO: Rename to process_media_sync/async — these methods now handle images, documents, and videos (not just images).
36983773
@classmethod
36993774
def process_image_sync(
3700-
cls, image_url: str, format: Optional[str] = None
3775+
cls,
3776+
image_url: str,
3777+
format: Optional[str] = None,
3778+
model: Optional[str] = None,
3779+
custom_llm_provider: Optional[str] = None,
37013780
) -> BedrockContentBlock:
3702-
"""Synchronous image processing."""
3781+
"""Synchronous processing of media URLs (images, documents, videos) for Bedrock."""
37033782

3704-
if "base64" in image_url:
3783+
if image_url.startswith("s3://"):
3784+
if model and not supports_s3_input(
3785+
model=model, custom_llm_provider=custom_llm_provider
3786+
):
3787+
raise ValueError(
3788+
f"Model '{model}' does not support s3:// URLs. "
3789+
"Only Amazon Nova models (with vision) support S3 input via Bedrock Converse API. "
3790+
"Please use a base64-encoded or https:// URL instead."
3791+
)
3792+
return cls._create_s3_bedrock_block(image_url, format)
3793+
elif "base64" in image_url:
37053794
img_bytes, mime_type, image_format = cls._parse_base64_image(image_url)
37063795
elif "http://" in image_url or "https://" in image_url:
37073796
img_bytes, mime_type = BedrockImageProcessor.get_image_details(image_url)
37083797
image_format = mime_type.split("/")[1]
37093798
else:
37103799
raise ValueError(
3711-
"Unsupported image type. Expected either image url or base64 encoded string"
3800+
"Unsupported image type. Expected either image url, base64 encoded string, or s3:// URL"
37123801
)
37133802

37143803
if format:
@@ -3720,11 +3809,25 @@ def process_image_sync(
37203809

37213810
@classmethod
37223811
async def process_image_async(
3723-
cls, image_url: str, format: Optional[str]
3812+
cls,
3813+
image_url: str,
3814+
format: Optional[str],
3815+
model: Optional[str] = None,
3816+
custom_llm_provider: Optional[str] = None,
37243817
) -> BedrockContentBlock:
3725-
"""Asynchronous image processing."""
3818+
"""Asynchronous processing of media URLs (images, documents, videos) for Bedrock."""
37263819

3727-
if "base64" in image_url:
3820+
if image_url.startswith("s3://"):
3821+
if model and not supports_s3_input(
3822+
model=model, custom_llm_provider=custom_llm_provider
3823+
):
3824+
raise ValueError(
3825+
f"Model '{model}' does not support s3:// URLs. "
3826+
"Only Amazon Nova models (with vision) support S3 input via Bedrock Converse API. "
3827+
"Please use a base64-encoded or https:// URL instead."
3828+
)
3829+
return cls._create_s3_bedrock_block(image_url, format)
3830+
elif "base64" in image_url:
37283831
img_bytes, mime_type, image_format = cls._parse_base64_image(image_url)
37293832
elif "http://" in image_url or "https://" in image_url:
37303833
img_bytes, mime_type = await BedrockImageProcessor.get_image_details_async(
@@ -3733,7 +3836,7 @@ async def process_image_async(
37333836
image_format = mime_type.split("/")[1]
37343837
else:
37353838
raise ValueError(
3736-
"Unsupported image type. Expected either image url or base64 encoded string"
3839+
"Unsupported image type. Expected either image url, base64 encoded string, or s3:// URL"
37373840
)
37383841

37393842
if format: # override with user-defined params
@@ -4374,14 +4477,30 @@ async def _bedrock_converse_messages_pt_async( # noqa: PLR0915
43744477
else:
43754478
image_url = element["image_url"]
43764479
_part = await BedrockImageProcessor.process_image_async( # type: ignore
4377-
image_url=image_url, format=format
4480+
image_url=image_url,
4481+
format=format,
4482+
model=model,
4483+
custom_llm_provider=llm_provider,
43784484
)
43794485
_parts.append(_part) # type: ignore
43804486
elif element["type"] == "file":
43814487
_part = await BedrockConverseMessagesProcessor._async_process_file_message(
4382-
message=cast(ChatCompletionFileObject, element)
4488+
message=cast(ChatCompletionFileObject, element),
4489+
model=model,
4490+
llm_provider=llm_provider,
43834491
)
43844492
_parts.append(_part)
4493+
elif element["type"] == "video_url":
4494+
video_url = BedrockImageProcessor._extract_video_url(
4495+
element
4496+
)
4497+
_part = await BedrockImageProcessor.process_image_async( # type: ignore
4498+
image_url=video_url,
4499+
format=None,
4500+
model=model,
4501+
custom_llm_provider=llm_provider,
4502+
)
4503+
_parts.append(_part) # type: ignore
43854504
_cache_point_block = (
43864505
litellm.AmazonConverseConfig()._get_cache_point_block(
43874506
message_block=cast(
@@ -4623,7 +4742,11 @@ def translate_thinking_blocks_to_reasoning_content_blocks(
46234742
return reasoning_content_blocks
46244743

46254744
@staticmethod
4626-
def _process_file_message(message: ChatCompletionFileObject) -> BedrockContentBlock:
4745+
def _process_file_message(
4746+
message: ChatCompletionFileObject,
4747+
model: Optional[str] = None,
4748+
llm_provider: Optional[str] = None,
4749+
) -> BedrockContentBlock:
46274750
file_message = message["file"]
46284751
file_data = file_message.get("file_data")
46294752
file_id = file_message.get("file_id")
@@ -4638,12 +4761,17 @@ def _process_file_message(message: ChatCompletionFileObject) -> BedrockContentBl
46384761
)
46394762
format = file_message.get("format")
46404763
return BedrockImageProcessor.process_image_sync(
4641-
image_url=cast(str, file_id or file_data), format=format
4764+
image_url=cast(str, file_id or file_data),
4765+
format=format,
4766+
model=model,
4767+
custom_llm_provider=llm_provider,
46424768
)
46434769

46444770
@staticmethod
46454771
async def _async_process_file_message(
46464772
message: ChatCompletionFileObject,
4773+
model: Optional[str] = None,
4774+
llm_provider: Optional[str] = None,
46474775
) -> BedrockContentBlock:
46484776
file_message = message["file"]
46494777
file_data = file_message.get("file_data")
@@ -4658,7 +4786,10 @@ async def _async_process_file_message(
46584786
llm_provider="bedrock",
46594787
)
46604788
return await BedrockImageProcessor.process_image_async(
4661-
image_url=cast(str, file_id or file_data), format=format
4789+
image_url=cast(str, file_id or file_data),
4790+
format=format,
4791+
model=model,
4792+
custom_llm_provider=llm_provider,
46624793
)
46634794

46644795
@staticmethod
@@ -4749,15 +4880,30 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915
47494880
_part = BedrockImageProcessor.process_image_sync( # type: ignore
47504881
image_url=image_url,
47514882
format=format,
4883+
model=model,
4884+
custom_llm_provider=llm_provider,
47524885
)
47534886
_parts.append(_part) # type: ignore
47544887
elif element["type"] == "file":
47554888
_part = (
47564889
BedrockConverseMessagesProcessor._process_file_message(
4757-
message=cast(ChatCompletionFileObject, element)
4890+
message=cast(ChatCompletionFileObject, element),
4891+
model=model,
4892+
llm_provider=llm_provider,
47584893
)
47594894
)
47604895
_parts.append(_part)
4896+
elif element["type"] == "video_url":
4897+
video_url = BedrockImageProcessor._extract_video_url(
4898+
element
4899+
)
4900+
_part = BedrockImageProcessor.process_image_sync( # type: ignore
4901+
image_url=video_url,
4902+
format=None,
4903+
model=model,
4904+
custom_llm_provider=llm_provider,
4905+
)
4906+
_parts.append(_part) # type: ignore
47614907
_cache_point_block = (
47624908
litellm.AmazonConverseConfig()._get_cache_point_block(
47634909
message_block=cast(

0 commit comments

Comments
 (0)