mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
5003a3a11b
commit
985574f3dc
9 changed files with 316 additions and 11 deletions
|
|
@ -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(
|
||||
|
|
|
|||
63
litellm/llms/bedrock/chat/tool_result_images.py
Normal file
63
litellm/llms/bedrock/chat/tool_result_images.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
98
tests/unit/llms/bedrock/chat/test_tool_result_images.py
Normal file
98
tests/unit/llms/bedrock/chat/test_tool_result_images.py
Normal 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"
|
||||
|
|
@ -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"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue