This commit is contained in:
ryan-crabbe-berri 2026-10-17 08:01:54 -07:00 • committed by GitHub
commit 88f2832480
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 184 additions and 59 deletions

View file

@ -49,6 +49,7 @@ _OPENAI_FAMILY_MODEL_RE: Final = re.compile(r"(^|[./])openai\.")
_OPENAI_GPT_VERSION_RE: Final = re.compile(r"(^|[./])openai\.gpt-(\d{1,3})(?!\d)(?:\.(\d{1,3})(?!\d))?")
_BEDROCK_RUNTIME_CHAT_COMPLETIONS_DEFAULT_SINCE: Final = (5, 6)
_XAI_GROK_MODEL_RE: Final = re.compile(r"(^|[./])xai\.grok-")
_CONVERSE_NESTED_TOOL_RESULT_IMAGE_RE: Final = re.compile(r"(^|[./])(anthropic\.claude|amazon\.nova)-")
_BEDROCK_RUNTIME_CHAT_COMPLETIONS_ENDPOINT: Final = "/v1/chat/completions"
BedrockRoute = Literal[
"converse",
@ -1021,14 +1022,19 @@ def bedrock_runtime_chat_completions_enforces_response_format(model: str) -> boo
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.
AWS documents that for Claude and Nova only (ToolResultContentBlock API reference, checked 2026-10-09),
so every other model gets the image beside the tool result. The price-map flag
``supports_bedrock_converse_tool_result_images`` overrides that in either direction.
"""
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)
flags: Final = tuple(
entry.get("supports_bedrock_converse_tool_result_images")
for entry in _bedrock_price_map_entries(model)
if entry is not None
)
explicit: Final = tuple(flag for flag in flags if isinstance(flag, bool))
if explicit:
return all(explicit)
return _CONVERSE_NESTED_TOOL_RESULT_IMAGE_RE.search(model) is not None
def bedrock_model_is_openai_gpt(model: str) -> bool:

View file

@ -59485,7 +59485,6 @@
"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,
@ -59522,7 +59521,6 @@
"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,
@ -59559,7 +59557,6 @@
"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,
@ -59596,7 +59593,6 @@
"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,
@ -59633,7 +59629,6 @@
"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,
@ -59667,7 +59662,6 @@
"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,
@ -59699,7 +59693,6 @@
"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,
@ -59731,7 +59724,6 @@
"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,
@ -59763,7 +59755,6 @@
"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,
@ -59798,7 +59789,6 @@
"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,
@ -59966,7 +59956,6 @@
"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,
@ -60007,7 +59996,6 @@
"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,
@ -60046,7 +60034,6 @@
"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,
@ -60091,7 +60078,6 @@
"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,
@ -60131,7 +60117,6 @@
"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,
@ -60166,7 +60151,6 @@
"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,
@ -60204,7 +60188,6 @@
"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,
@ -60239,7 +60222,6 @@
"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,
@ -73808,7 +73790,6 @@
"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,
@ -73902,7 +73883,6 @@
"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

@ -59485,7 +59485,6 @@
"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,
@ -59522,7 +59521,6 @@
"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,
@ -59559,7 +59557,6 @@
"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,
@ -59596,7 +59593,6 @@
"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,
@ -59633,7 +59629,6 @@
"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,
@ -59667,7 +59662,6 @@
"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,
@ -59699,7 +59693,6 @@
"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,
@ -59731,7 +59724,6 @@
"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,
@ -59763,7 +59755,6 @@
"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,
@ -59798,7 +59789,6 @@
"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,
@ -59966,7 +59956,6 @@
"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,
@ -60007,7 +59996,6 @@
"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,
@ -60046,7 +60034,6 @@
"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,
@ -60091,7 +60078,6 @@
"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,
@ -60131,7 +60117,6 @@
"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,
@ -60166,7 +60151,6 @@
"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,
@ -60204,7 +60188,6 @@
"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,
@ -60239,7 +60222,6 @@
"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,
@ -73808,7 +73790,6 @@
"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,
@ -73902,7 +73883,6 @@
"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

@ -3,7 +3,18 @@ from __future__ import annotations
from typing import Final
import pytest
from anthropic.types import RawContentBlockDeltaEvent, RawMessageDeltaEvent, TextBlock, TextDelta, ToolParam
from _pytest.mark.structures import ParameterSet
from anthropic import Anthropic
from anthropic.types import (
Message,
MessageParam,
RawContentBlockDeltaEvent,
RawMessageDeltaEvent,
TextBlock,
TextDelta,
ToolParam,
ToolUseBlock,
)
from e2e_config import unique_marker
from e2e_metadata import Capability, Domain, Mode, Provider, Route, Subject, meta
from lifecycle import ResourceManager
@ -17,6 +28,59 @@ pytestmark = pytest.mark.e2e
GPT_6_1_SOL_BACKEND: Final = "bedrock/global.openai.gpt-6.1-sol"
CONVERSE_CLAUDE_BACKEND: Final = "bedrock/converse/us.anthropic.claude-haiku-4-5-20251001-v1:0"
NOVA_BACKEND: Final = "bedrock/us.amazon.nova-2-lite-v1:0"
TOOL_RESULT_IMAGE_CLAUDE_BACKEND: Final = "bedrock/converse/us.anthropic.claude-haiku-5-5"
TOOL_RESULT_IMAGE_BACKENDS: Final = (
GPT_6_1_SOL_BACKEND,
"bedrock/global.openai.gpt-6-sol",
"bedrock/global.openai.gpt-5.6-sol",
"bedrock/global.openai.gpt-5.5",
"bedrock/global.openai.gpt-5.4",
"bedrock/qwen.qwen3-vl-235b-a22b",
"bedrock/global.moonshotai.kimi-k3",
"bedrock/mistral.mistral-large-3-675b-instruct",
TOOL_RESULT_IMAGE_CLAUDE_BACKEND,
NOVA_BACKEND,
)
RED_PIXEL_PNG_B64: Final = (
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
)
READ_TOOL: Final[ToolParam] = {
"name": "Read",
"description": "Read a file from the local filesystem.",
"input_schema": {
"type": "object",
"properties": {"file_path": {"type": "string"}},
"required": ["file_path"],
},
}
READ_TOOL_USE_ID: Final = "toolu_01A09q90qw90lq917835lq9"
READ_IMAGE_CONVERSATION: Final[tuple[MessageParam, ...]] = (
{"role": "user", "content": "What color is the image in pixel.png? Answer in one word."},
{
"role": "assistant",
"content": [{"type": "tool_use", "id": READ_TOOL_USE_ID, "name": "Read", "input": {"file_path": "pixel.png"}}],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": READ_TOOL_USE_ID,
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": RED_PIXEL_PNG_B64},
}
],
}
],
},
)
FOLLOW_UP_TURNS: Final[tuple[MessageParam, ...]] = (
{"role": "assistant", "content": "The file holds a single pixel."},
{"role": "user", "content": "Thanks. Reply with the word done."},
)
TOOL_SEARCH: Final[ToolParam] = {
"name": "ToolSearch",
@ -62,6 +126,39 @@ def _register(proxy: ProxyClient, resources: ResourceManager, backend: str) -> s
return model
def _tool_result_image_params() -> list[ParameterSet]:
return [
pytest.param(
backend,
id=backend.rsplit("/", 1)[-1],
marks=meta(
Subject(
domain=Domain.LLM_TRANSLATION,
route=Route.MESSAGES,
providers=(Provider.BEDROCK,),
models=(backend,),
capabilities=(Capability.FUNCTION_CALLING, Capability.VISION),
mode=Mode.NONSTREAM,
)
),
)
for backend in TOOL_RESULT_IMAGE_BACKENDS
]
def _reply(client: Anthropic, model: str, messages: tuple[MessageParam, ...]) -> Message:
return client.messages.create(
model=model, max_tokens=2048, tools=[READ_TOOL], messages=messages, extra_body=NO_PROXY_CACHE
)
def _answered(message: Message) -> bool:
return any(
isinstance(block, ToolUseBlock) or (isinstance(block, TextBlock) and block.text.strip())
for block in message.content
)
class TestBedrockMessages:
@meta(
Subject(
@ -175,3 +272,24 @@ class TestBedrockMessages:
assert len(deltas) == 1, f"expected exactly one message_delta: {types}"
assert deltas[0].delta.stop_reason is not None, f"message_delta carried no stop_reason: {deltas[0]!r}"
assert deltas[0].usage.output_tokens > 0, f"message_delta reported no output tokens: {deltas[0]!r}"
class TestBedrockToolResultImages:
@pytest.mark.parametrize("backend", _tool_result_image_params())
def test_image_returned_by_a_tool_gets_a_reply(
self, proxy: ProxyClient, resources: ResourceManager, sdk: SdkClients, backend: str
) -> None:
"""Claude Code's Read tool returns an image file as an image inside tool_result.
Converse documents that nesting for Claude and Nova only and rejects it for other
models (#45576), so the proxy has to reshape it for them. The image stays in
history on every later turn, so the follow-up must be accepted too or the session
is stuck. Some models answer by calling Read again, which is still a well-formed
next turn."""
model: Final = _register(proxy, resources, backend)
client: Final = sdk.anthropic(resources.key())
tool_result_reply: Final = _reply(client, model, READ_IMAGE_CONVERSATION)
assert _answered(tool_result_reply), f"{backend} sent back no text or tool call: {tool_result_reply!r}"
follow_up_reply: Final = _reply(client, model, (*READ_IMAGE_CONVERSATION, *FOLLOW_UP_TURNS))
assert _answered(follow_up_reply), f"{backend} sent back no text or tool call: {follow_up_reply!r}"

View file

@ -1,5 +1,8 @@
from typing import Final
import pytest
import litellm
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 (
@ -14,26 +17,64 @@ _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(
def _nested_image_message() -> MessageBlock:
return MessageBlock(
role="user",
content=[
ContentBlock(
toolResult=ToolResultBlock(
toolUseId="tooluse_nested",
content=[
ToolResultContentBlock(text="nested", image=_TOOL_IMAGE),
],
content=[ToolResultContentBlock(text="nested"), ToolResultContentBlock(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 _keeps_image_nested(placed: tuple[MessageBlock, ...], message: MessageBlock) -> bool:
if placed[0] is message:
return True
tool_result: Final = placed[0]["content"][0]["toolResult"]
assert isinstance(tool_result, dict)
assert tool_result["content"] == [{"text": "nested"}]
assert placed[0]["content"][1] == {"image": _TOOL_IMAGE}
return False
@pytest.mark.parametrize(
("model", "nested"),
[
("bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0", True),
("bedrock/converse/anthropic.claude-sonnet-4-5-20250929-v1:0", True),
("bedrock/us.amazon.nova-pro-v1:0", True),
("bedrock/converse/us.amazon.nova-2-lite-v1:0", True),
("bedrock/global.openai.gpt-6.1-sol", False),
("bedrock/qwen.qwen3-vl-235b-a22b", False),
("bedrock/global.moonshotai.kimi-k3", False),
("bedrock/mistral.mistral-large-3-675b-instruct", False),
("bedrock/converse/arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123", False),
],
)
def test_place_tool_result_images_nests_only_for_claude_and_nova(model: str, nested: bool):
message: Final = _nested_image_message()
assert _keeps_image_nested(place_tool_result_images([message], model), message) is nested
@pytest.mark.parametrize(
("price_map_key", "flag", "nested"),
[
("qwen.qwen3-vl-235b-a22b", True, True),
("anthropic.claude-haiku-4-5-20251001-v1:0", False, False),
],
)
def test_price_map_flag_overrides_the_model_family(
monkeypatch: pytest.MonkeyPatch, price_map_key: str, flag: bool, nested: bool
):
entry: Final = {**litellm.model_cost.get(price_map_key, {}), "supports_bedrock_converse_tool_result_images": flag}
monkeypatch.setitem(litellm.model_cost, price_map_key, entry)
message: Final = _nested_image_message()
placed: Final = place_tool_result_images([message], f"bedrock/{price_map_key}")
assert _keeps_image_nested(placed, message) is nested
def test_place_tool_result_images_leaves_text_only_tool_results_on_gpt():