refactor(bedrock): share the sync remote media inliner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo-berri 2026-09-26 18:41:15 +00:00 • committed by mateo
parent 093a9d4ddf
commit daba2576f5
4 changed files with 69 additions and 46 deletions

View file

@ -310,6 +310,26 @@ async def _fetch_data_urls(remote_urls: tuple[str, ...]) -> tuple[str, ...]:
raise
def inline_remote_media(
messages: list[AllMessageValues], # mutable-ok: every transform_request takes list[AllMessageValues]
should_inline: Callable[[RemoteMedia], bool] = inline_every_remote_url,
) -> list[AllMessageValues]: # mutable-ok: every transform_request takes list[AllMessageValues]
remote_urls: Final = tuple(
dict.fromkeys(
remote.url
for message in messages
for part in _content_parts(message)
if (remote := _parse_remote_part(part)) is not None and should_inline(_remote_media(remote))
)
)
if not remote_urls:
return messages
data_urls: Final = MappingProxyType({url: convert_url_to_base64(url) for url in remote_urls})
return [ # mutable-ok: transform_request takes a list
_inline_message(message, data_urls, should_inline) for message in messages
]
async def async_inline_remote_media(
messages: list[AllMessageValues], # mutable-ok: every transform_request takes list[AllMessageValues]
should_inline: Callable[[RemoteMedia], bool] = inline_every_remote_url,

View file

@ -24,8 +24,8 @@ from typing_extensions import assert_never
import litellm
from litellm.litellm_core_utils.prompt_templates.image_handling import (
async_inline_remote_media,
convert_url_to_base64,
inline_remote_image_urls,
inline_remote_media,
)
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
@ -172,48 +172,6 @@ def split_reasoning_tag(content: str) -> tuple[str | None, str]:
return reasoning or None, body
def _remote_http_url(candidate: object) -> str | None:
return candidate if isinstance(candidate, str) and candidate.startswith(("http://", "https://")) else None
def _inlined_image_url_part(part: object) -> object:
fields: Final = part if isinstance(part, Mapping) else None
if fields is None or fields.get("type") != "image_url":
return part
image_url: Final = fields.get("image_url")
image_url_fields: Final = image_url if isinstance(image_url, Mapping) else None
url: Final = _remote_http_url(image_url_fields.get("url") if image_url_fields is not None else image_url)
if url is None:
return part
data_url: Final = convert_url_to_base64(url)
inlined: Final = {**image_url_fields, "url": data_url} if image_url_fields is not None else data_url
return {**fields, "image_url": inlined} # mutable-ok: json-serialized message part
def _inlined_image_url_message(message: AllMessageValues) -> AllMessageValues:
content: Final = message.get("content")
if not isinstance(content, list):
return message
inlined_message: Final = { # mutable-ok: json-serialized message
**message,
"content": [_inlined_image_url_part(part) for part in content],
}
return inlined_message # pyright: ignore[reportReturnType] # the same message with remote image parts inlined
def _with_inlined_remote_image_urls(
messages: list[AllMessageValues],
) -> list[AllMessageValues]: # mutable-ok: transform_request takes a list
"""Inline every remote ``image_url`` so AWS never sees the ``http(s)://`` URLs it rejects.
AWS's native surface only takes inline ``data:`` URLs and S3 URLs where Converse downloaded
remote images itself, so the bytes are fetched and inlined here exactly like Converse did.
"""
return [ # mutable-ok: transform_request takes a list
_inlined_image_url_message(message) for message in messages
]
class BedrockRuntimeChatCompletionsStreamingHandler(OpenAIChatCompletionStreamingHandler):
"""OpenAI chunk parsing plus the ``<reasoning>`` split, tracked per choice index."""
@ -376,7 +334,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig):
) -> dict: # mutable-ok: BaseConfig signature
return super().transform_request(
model=split_bedrock_region_path(model)[1],
messages=_with_inlined_remote_image_urls(messages),
messages=inline_remote_media(messages, should_inline=inline_remote_image_urls),
optional_params=self._inference_params(optional_params),
litellm_params=litellm_params,
headers=headers,

View file

@ -16,6 +16,7 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
async_convert_url_to_base64,
async_inline_remote_media,
convert_url_to_base64,
inline_remote_media,
)
from litellm.litellm_core_utils.url_utils import SSRFError
@ -320,6 +321,50 @@ async def test_async_inline_remote_media_inlines_every_remote_part_shape(async_o
assert messages == snapshot
def test_inline_remote_media_inlines_every_remote_part_shape(monkeypatch):
image_url = f"http://img.example/{uuid.uuid4()}.png"
pdf_url = f"http://docs.example/{uuid.uuid4()}.pdf"
fetched = []
def fake_convert(url):
fetched.append(url)
return f"data:image/png;base64,{url}"
monkeypatch.setattr(image_handling, "convert_url_to_base64", fake_convert)
messages = [
{"role": "system", "content": "be terse"},
{
"role": "user",
"content": [
{"type": "text", "text": "what is this?"},
{"type": "image_url", "image_url": {"url": image_url, "detail": "low"}},
{"type": "image_url", "image_url": image_url},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}},
{"type": "image_url", "image_url": {"url": "s3://bucket/key.png"}},
{"type": "file", "file": {"file_id": pdf_url}},
{"type": "document", "source": {"type": "url", "url": pdf_url}, "title": "the doc"},
],
},
]
snapshot = copy.deepcopy(messages)
inlined = inline_remote_media(messages, should_inline=image_handling.inline_remote_image_urls)
data_url = f"data:image/png;base64,{image_url}"
assert inlined[0] == {"role": "system", "content": "be terse"}
assert inlined[1]["content"] == [
{"type": "text", "text": "what is this?"},
{"type": "image_url", "image_url": {"url": data_url, "detail": "low"}},
{"type": "image_url", "image_url": data_url},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}},
{"type": "image_url", "image_url": {"url": "s3://bucket/key.png"}},
{"type": "file", "file": {"file_id": pdf_url}},
{"type": "document", "source": {"type": "url", "url": pdf_url}, "title": "the doc"},
]
assert fetched == [image_url]
assert messages == snapshot
async def test_async_inline_remote_media_inlines_only_the_parts_the_predicate_accepts(async_only_image_fetch):
files_api_prefix = "https://generativelanguage.googleapis.com/v1beta/files/"
files_api_pdf = f"{files_api_prefix}{uuid.uuid4().hex}"

View file

@ -360,10 +360,10 @@ def _assert_remote_images_inlined(content):
def test_transform_request_inlines_remote_image_urls(local_cost_map, monkeypatch):
import litellm.llms.bedrock.chat.chat_completions.transformation as native_cc
import litellm.litellm_core_utils.prompt_templates.image_handling as image_handling
monkeypatch.setattr(
native_cc, "convert_url_to_base64", lambda url: f"data:image/png;base64,{url}"
image_handling, "convert_url_to_base64", lambda url: f"data:image/png;base64,{url}"
)
body = AmazonBedrockRuntimeChatCompletionsConfig().transform_request(
model="us.xai.grok-4.6",