fix(bedrock): keep GPT tool-result images beside the tool result (#45579)

* fix(bedrock): keep GPT tool-result images beside the tool result

Bedrock gpt-6.1-sol rejects an image nested in toolResult.content. Put it on the same user turn instead.

* fix(bedrock): read tool-result image support from the price map

OpenAI GPT rows on Bedrock Converse reject an image inside toolResult.content, so those images sit beside the tool result.

* fix: sync price-map test schema and import order

CI validates model_prices_and_context_window.json against an inline schema in test_utils, and ruff checks import order in converse_transformation.

* fix(bedrock): satisfy basedpyright in tool-result image helper

Drop Final assignments inside loops and use TypedDict key checks so the lint type-check gate stays within its merge-base ceiling.

* test(bedrock): cover tool-result image placement helper

Exercise Claude no-op paths, text-only GPT tool results, status preservation, and split text/image tool content for codecov.

* refactor(bedrock): reuse the shared tool-result image placeholder text

---------

Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
This commit is contained in:
Shubham Jain 2026-10-10 02:02:23 +01:00 • committed by GitHub
parent 5003a3a11b
commit 985574f3dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 316 additions and 11 deletions

View file

@ -52,6 +52,7 @@ from litellm.llms.anthropic.chat.transformation import (
)
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.llms.bedrock.chat.tool_result_images import place_tool_result_images
from litellm.llms.bedrock.common_utils import bedrock_model_supports_regex_lookaround
from litellm.llms.bedrock.request_metadata import (
bedrock_request_metadata_headers,
@ -2037,11 +2038,16 @@ class AmazonConverseConfig(BaseConfig):
litellm_params=litellm_params,
)
bedrock_messages: Final = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async(
messages=messages,
model=model,
llm_provider="bedrock_converse",
user_continue_message=litellm_params.pop("user_continue_message", None),
bedrock_messages: Final = list(
place_tool_result_images(
await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async(
messages=messages,
model=model,
llm_provider="bedrock_converse",
user_continue_message=litellm_params.pop("user_continue_message", None),
),
model,
)
)
request_metadata: Final = resolve_bedrock_request_metadata(
@ -2100,11 +2106,16 @@ class AmazonConverseConfig(BaseConfig):
)
## TRANSFORMATION ##
bedrock_messages: Final[list[MessageBlock]] = _bedrock_converse_messages_pt(
messages=messages,
model=model,
llm_provider="bedrock_converse",
user_continue_message=litellm_params.pop("user_continue_message", None),
bedrock_messages: Final[list[MessageBlock]] = list(
place_tool_result_images(
_bedrock_converse_messages_pt(
messages=messages,
model=model,
llm_provider="bedrock_converse",
user_continue_message=litellm_params.pop("user_continue_message", None),
),
model,
)
)
request_metadata: Final = resolve_bedrock_request_metadata(

View file

@ -0,0 +1,63 @@
"""Place tool-result images where Bedrock Converse will accept them."""
from collections.abc import Iterator, Sequence
from typing import Final
from litellm.litellm_core_utils.prompt_templates.common_utils import TOOL_RESULT_IMAGE_PLACEHOLDER
from litellm.llms.bedrock.common_utils import bedrock_converse_supports_tool_result_images
from litellm.types.llms.bedrock import ContentBlock, ImageBlock, MessageBlock, ToolResultBlock, ToolResultContentBlock
def place_tool_result_images(messages: Sequence[MessageBlock], model: str) -> tuple[MessageBlock, ...]:
"""Move images out of ``toolResult.content`` when this model rejects them there."""
if bedrock_converse_supports_tool_result_images(model):
return tuple(messages)
return tuple(_message_with_sibling_images(message) for message in messages)
def _message_with_sibling_images(message: MessageBlock) -> MessageBlock:
content: Final = tuple(message.get("content") or ())
rewritten: Final = tuple(_blocks_with_sibling_images(content))
if rewritten == content:
return message
return MessageBlock(role=message["role"], content=list(rewritten))
def _blocks_with_sibling_images(content: Sequence[ContentBlock]) -> Iterator[ContentBlock]:
for block in content:
tool_result = block.get("toolResult")
if tool_result is None:
yield block
continue
parts = tuple(tool_result.get("content") or ())
images = tuple(_images(parts))
if not images:
yield block
continue
kept = tuple(_without_images(parts)) or (ToolResultContentBlock(text=TOOL_RESULT_IMAGE_PLACEHOLDER),)
yield ContentBlock(toolResult=_tool_result_without_images(tool_result, kept))
yield from (ContentBlock(image=image) for image in images)
def _images(parts: Sequence[ToolResultContentBlock]) -> Iterator[ImageBlock]:
for part in parts:
if "image" in part:
yield part["image"]
def _without_images(parts: Sequence[ToolResultContentBlock]) -> Iterator[ToolResultContentBlock]:
for part in parts:
if "image" not in part:
yield part
def _tool_result_without_images(
tool_result: ToolResultBlock,
kept: Sequence[ToolResultContentBlock],
) -> ToolResultBlock:
content: Final = list(kept)
tool_use_id: Final = tool_result["toolUseId"]
status: Final = tool_result.get("status")
if status is None:
return ToolResultBlock(content=content, toolUseId=tool_use_id)
return ToolResultBlock(content=content, toolUseId=tool_use_id, status=status)

View file

@ -923,6 +923,19 @@ def bedrock_runtime_chat_completions_enforces_response_format(model: str) -> boo
return _bedrock_price_map_flag(model, "supports_bedrock_runtime_chat_completions_response_format")
def bedrock_converse_supports_tool_result_images(model: str) -> bool:
"""Whether Converse accepts an image nested in ``toolResult.content``.
Missing means yes, which is what Claude accepts. A price-map row sets
``supports_bedrock_converse_tool_result_images`` to false when Bedrock
rejects that image and it has to sit beside the tool result instead.
"""
entries: Final = tuple(entry for entry in _bedrock_price_map_entries(model) if entry is not None)
if not entries:
return True
return all(entry.get("supports_bedrock_converse_tool_result_images") is not False for entry in entries)
def bedrock_model_is_openai_gpt(model: str) -> bool:
"""A GPT-5.x or GPT-6.x id, never GPT-OSS: the families whose sampling params AWS ties to reasoning being off."""
return _openai_gpt_version(model) is not None

View file

@ -56693,6 +56693,7 @@
"output_cost_per_token": 2.2e-05,
"output_cost_per_token_above_272k_tokens": 3.3e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56729,6 +56730,7 @@
"output_cost_per_token": 2e-05,
"output_cost_per_token_above_272k_tokens": 3e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56765,6 +56767,7 @@
"output_cost_per_token": 1.32e-05,
"output_cost_per_token_above_272k_tokens": 1.98e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56801,6 +56804,7 @@
"output_cost_per_token": 1.2e-05,
"output_cost_per_token_above_272k_tokens": 1.8e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56837,6 +56841,7 @@
"output_cost_per_token": 1.32e-06,
"output_cost_per_token_above_272k_tokens": 1.98e-06,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56870,6 +56875,7 @@
"output_cost_per_token": 1.65e-05,
"output_cost_per_token_above_272k_tokens": 2.475e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56901,6 +56907,7 @@
"output_cost_per_token": 1.5e-05,
"output_cost_per_token_above_272k_tokens": 2.25e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56932,6 +56939,7 @@
"output_cost_per_token": 3.3e-05,
"output_cost_per_token_above_272k_tokens": 4.95e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56963,6 +56971,7 @@
"output_cost_per_token": 3e-05,
"output_cost_per_token_above_272k_tokens": 4.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56997,6 +57006,7 @@
"output_cost_per_token": 1.2e-06,
"output_cost_per_token_above_272k_tokens": 1.8e-06,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57164,6 +57174,7 @@
"output_cost_per_token": 5.5e-05,
"output_cost_per_token_above_272k_tokens": 8.25e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57204,6 +57215,7 @@
"output_cost_per_token": 1.1e-05,
"output_cost_per_token_above_272k_tokens": 1.65e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57242,6 +57254,7 @@
"output_cost_per_token": 5.5e-07,
"output_cost_per_token_above_272k_tokens": 8.25e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57286,6 +57299,7 @@
"output_cost_per_token": 5e-05,
"output_cost_per_token_above_272k_tokens": 7.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57325,6 +57339,7 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_above_272k_tokens": 1.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57359,6 +57374,7 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_above_272k_tokens": 1.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57396,6 +57412,7 @@
"output_cost_per_token": 5e-07,
"output_cost_per_token_above_272k_tokens": 7.5e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57430,6 +57447,7 @@
"output_cost_per_token": 5e-07,
"output_cost_per_token_above_272k_tokens": 7.5e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -70505,6 +70523,7 @@
"input_cost_per_token_above_272k_tokens_ultrafast": 2.4e-05,
"input_cost_per_token_ultrafast": 1.2e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1000000,
"max_output_tokens": 131072,
"max_tokens": 131072,
@ -70598,6 +70617,7 @@
"input_cost_per_token_above_272k_tokens_ultrafast": 2.64e-05,
"input_cost_per_token_ultrafast": 1.32e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1000000,
"max_output_tokens": 131072,
"max_tokens": 131072,

View file

@ -56693,6 +56693,7 @@
"output_cost_per_token": 2.2e-05,
"output_cost_per_token_above_272k_tokens": 3.3e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56729,6 +56730,7 @@
"output_cost_per_token": 2e-05,
"output_cost_per_token_above_272k_tokens": 3e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56765,6 +56767,7 @@
"output_cost_per_token": 1.32e-05,
"output_cost_per_token_above_272k_tokens": 1.98e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56801,6 +56804,7 @@
"output_cost_per_token": 1.2e-05,
"output_cost_per_token_above_272k_tokens": 1.8e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56837,6 +56841,7 @@
"output_cost_per_token": 1.32e-06,
"output_cost_per_token_above_272k_tokens": 1.98e-06,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56870,6 +56875,7 @@
"output_cost_per_token": 1.65e-05,
"output_cost_per_token_above_272k_tokens": 2.475e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56901,6 +56907,7 @@
"output_cost_per_token": 1.5e-05,
"output_cost_per_token_above_272k_tokens": 2.25e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56932,6 +56939,7 @@
"output_cost_per_token": 3.3e-05,
"output_cost_per_token_above_272k_tokens": 4.95e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56963,6 +56971,7 @@
"output_cost_per_token": 3e-05,
"output_cost_per_token_above_272k_tokens": 4.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -56997,6 +57006,7 @@
"output_cost_per_token": 1.2e-06,
"output_cost_per_token_above_272k_tokens": 1.8e-06,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57164,6 +57174,7 @@
"output_cost_per_token": 5.5e-05,
"output_cost_per_token_above_272k_tokens": 8.25e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57204,6 +57215,7 @@
"output_cost_per_token": 1.1e-05,
"output_cost_per_token_above_272k_tokens": 1.65e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57242,6 +57254,7 @@
"output_cost_per_token": 5.5e-07,
"output_cost_per_token_above_272k_tokens": 8.25e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57286,6 +57299,7 @@
"output_cost_per_token": 5e-05,
"output_cost_per_token_above_272k_tokens": 7.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57325,6 +57339,7 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_above_272k_tokens": 1.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57359,6 +57374,7 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_above_272k_tokens": 1.5e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57396,6 +57412,7 @@
"output_cost_per_token": 5e-07,
"output_cost_per_token_above_272k_tokens": 7.5e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -57430,6 +57447,7 @@
"output_cost_per_token": 5e-07,
"output_cost_per_token_above_272k_tokens": 7.5e-07,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1050000,
"max_output_tokens": 128000,
"max_tokens": 128000,
@ -70505,6 +70523,7 @@
"input_cost_per_token_above_272k_tokens_ultrafast": 2.4e-05,
"input_cost_per_token_ultrafast": 1.2e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1000000,
"max_output_tokens": 131072,
"max_tokens": 131072,
@ -70598,6 +70617,7 @@
"input_cost_per_token_above_272k_tokens_ultrafast": 2.64e-05,
"input_cost_per_token_ultrafast": 1.32e-05,
"litellm_provider": "bedrock_converse",
"supports_bedrock_converse_tool_result_images": false,
"max_input_tokens": 1000000,
"max_output_tokens": 131072,
"max_tokens": 131072,

View file

@ -984,6 +984,9 @@
"supports_audio_output": {
"type": "boolean"
},
"supports_bedrock_converse_tool_result_images": {
"type": "boolean"
},
"supports_bedrock_runtime_chat_completions_inline_reasoning": {
"type": "boolean"
},

View file

@ -17,7 +17,8 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig
from litellm.llms.custom_httpx.http_handler import HTTPHandler
from litellm.types.llms.bedrock import ConverseTokenUsageBlock
from litellm.types.llms.bedrock import ContentBlock, ConverseTokenUsageBlock
from litellm.types.llms.openai import AllMessageValues
def test_transform_usage():
@ -6558,6 +6559,81 @@ def test_bedrock_tool_message_image_url_png_still_becomes_image():
assert block["image"]["source"]["bytes"] == png_b64
def _png_tool_messages(text: str | None = None) -> list[AllMessageValues]:
from litellm.types.llms.openai import (
ChatCompletionImageObject,
ChatCompletionImageUrlObject,
ChatCompletionTextObject,
ChatCompletionToolMessage,
ChatCompletionUserMessage,
)
png_b64: Final = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABXvMqOgAAAABJRU5ErkJggg=="
image: Final = ChatCompletionImageObject(
type="image_url",
image_url=ChatCompletionImageUrlObject(url=f"data:image/png;base64,{png_b64}"),
)
parts: Final = (
(ChatCompletionTextObject(type="text", text=text), image) if text is not None else (image,)
)
user: Final = ChatCompletionUserMessage(role="user", content="Describe the attached image.")
tool: Final = ChatCompletionToolMessage(role="tool", tool_call_id="tooluse_png_gpt", content=list(parts))
return [user, tool]
def _gpt_tool_turn(text: str | None) -> list[ContentBlock]:
from litellm.types.llms.openai import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
fetch_tool: Final = ChatCompletionToolParam(
type="function",
function=ChatCompletionToolParamFunctionChunk(
name="fetch_image",
description="Returns an image",
parameters={"type": "object", "properties": {}},
),
)
body: Final = AmazonConverseConfig()._transform_request(
model="bedrock/global.openai.gpt-6.1-sol",
messages=_png_tool_messages(text),
optional_params={"tools": [fetch_tool]},
litellm_params={},
headers={},
)
messages: Final = body["messages"]
assert isinstance(messages, list)
last: Final = messages[-1]
assert isinstance(last, dict)
content: Final = last["content"]
assert isinstance(content, list)
return [ContentBlock(**block) for block in content if isinstance(block, dict)]
def test_openai_gpt_tool_result_image_sits_beside_the_tool_result():
"""gpt-6.1-sol rejects an image nested in toolResult.content. The image has to be a sibling block."""
turn: Final = _gpt_tool_turn("tool image")
tool_index: Final = next(i for i, block in enumerate(turn) if "toolResult" in block)
tool_result: Final = turn[tool_index]["toolResult"]
assert isinstance(tool_result, dict)
assert tool_result["toolUseId"] == "tooluse_png_gpt"
assert tool_result["content"] == [{"text": "tool image"}]
image: Final = turn[tool_index + 1]["image"]
assert isinstance(image, dict)
assert image["format"] == "png"
def test_openai_gpt_image_only_tool_result_keeps_a_text_block():
from litellm.litellm_core_utils.prompt_templates.common_utils import TOOL_RESULT_IMAGE_PLACEHOLDER
turn: Final = _gpt_tool_turn(None)
tool_index: Final = next(i for i, block in enumerate(turn) if "toolResult" in block)
tool_result: Final = turn[tool_index]["toolResult"]
assert isinstance(tool_result, dict)
assert tool_result["content"] == [{"text": TOOL_RESULT_IMAGE_PLACEHOLDER}]
image: Final = turn[tool_index + 1]["image"]
assert isinstance(image, dict)
assert image["format"] == "png"
def test_transform_response_does_not_leak_body_on_parse_failure():
from litellm.llms.bedrock.common_utils import BedrockError

View file

@ -0,0 +1,98 @@
from typing import Final
from litellm.litellm_core_utils.prompt_templates.common_utils import TOOL_RESULT_IMAGE_PLACEHOLDER
from litellm.llms.bedrock.chat.tool_result_images import place_tool_result_images
from litellm.types.llms.bedrock import (
ContentBlock,
ImageBlock,
MessageBlock,
ToolResultBlock,
ToolResultContentBlock,
)
_PNG_BYTES: Final = b"\x89PNG\r\n\x1a\n"
_TOOL_IMAGE: Final = ImageBlock(format="png", source={"bytes": _PNG_BYTES})
def test_place_tool_result_images_keeps_nested_images_for_claude():
message: Final = MessageBlock(
role="user",
content=[
ContentBlock(
toolResult=ToolResultBlock(
toolUseId="tooluse_nested",
content=[
ToolResultContentBlock(text="nested", image=_TOOL_IMAGE),
],
),
),
],
)
placed: Final = place_tool_result_images(
[message],
"bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0",
)
assert placed == (message,)
assert placed[0] is message
def test_place_tool_result_images_leaves_text_only_tool_results_on_gpt():
message: Final = MessageBlock(
role="user",
content=[
ContentBlock(text="plain user text"),
ContentBlock(
toolResult=ToolResultBlock(
toolUseId="tooluse_text",
content=[ToolResultContentBlock(text="only text")],
),
),
],
)
placed: Final = place_tool_result_images([message], "bedrock/global.openai.gpt-6.1-sol")
assert placed[0] is message
def test_place_tool_result_images_keeps_text_when_image_is_a_separate_part():
message: Final = MessageBlock(
role="user",
content=[
ContentBlock(
toolResult=ToolResultBlock(
toolUseId="tooluse_split",
content=[
ToolResultContentBlock(text="failed"),
ToolResultContentBlock(image=_TOOL_IMAGE),
],
),
),
],
)
placed: Final = place_tool_result_images([message], "bedrock/global.openai.gpt-6.1-sol")
tool_result: Final = placed[0]["content"][0]["toolResult"]
assert isinstance(tool_result, dict)
assert tool_result["content"] == [{"text": "failed"}]
assert "status" not in tool_result
def test_place_tool_result_images_preserves_tool_result_status_on_gpt():
message: Final = MessageBlock(
role="user",
content=[
ContentBlock(
toolResult=ToolResultBlock(
toolUseId="tooluse_status",
status="error",
content=[ToolResultContentBlock(image=_TOOL_IMAGE)],
),
),
],
)
placed: Final = place_tool_result_images([message], "bedrock/global.openai.gpt-6.1-sol")
tool_result: Final = placed[0]["content"][0]["toolResult"]
assert isinstance(tool_result, dict)
assert tool_result["status"] == "error"
assert tool_result["content"] == [{"text": TOOL_RESULT_IMAGE_PLACEHOLDER}]
image: Final = placed[0]["content"][1]["image"]
assert isinstance(image, dict)
assert image["format"] == "png"

View file

@ -1022,6 +1022,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"supports_bedrock_runtime_chat_completions_tools_with_reasoning": {"type": "boolean"},
"supports_bedrock_runtime_chat_completions_response_format": {"type": "boolean"},
"supports_bedrock_runtime_chat_completions_inline_reasoning": {"type": "boolean"},
"supports_bedrock_converse_tool_result_images": {"type": "boolean"},
"supports_url_context": {"type": "boolean"},
"supports_multimodal": {"type": "boolean"},
"uses_embed_content": {"type": "boolean"},