diff --git a/litellm/llms/bedrock/chat/agentcore/transformation.py b/litellm/llms/bedrock/chat/agentcore/transformation.py index a18d2b32e6a..6c07a6f29bd 100644 --- a/litellm/llms/bedrock/chat/agentcore/transformation.py +++ b/litellm/llms/bedrock/chat/agentcore/transformation.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Final, Optional, Union from urllib.parse import quote import httpx +from pydantic import TypeAdapter from litellm._logging import verbose_logger from litellm._uuid import uuid @@ -50,6 +51,9 @@ else: HTTPHandler = Any AsyncHTTPHandler = Any +_PARAMS: Final = TypeAdapter(dict[str, object]) +_TOOLS: Final = TypeAdapter(tuple[object, ...]) + class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM): def __init__(self, **kwargs): @@ -267,7 +271,10 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM): # list verbatim under "content" so an attachment-aware agent can read the raw # blocks (image_url, file, etc.). Default off keeps the payload byte-identical # to the legacy {"prompt": "..."} shape for agents that only read the prompt. - if self._is_flag_enabled(optional_params, litellm_params, "forward_multimodal_content"): + typed_optional_params: Final = _PARAMS.validate_python(optional_params) + typed_litellm_params: Final = _PARAMS.validate_python(litellm_params) + + if self._is_flag_enabled(typed_optional_params, typed_litellm_params, "forward_multimodal_content"): last_content: Final = messages[-1].get("content") if isinstance(last_content, list) and any( isinstance(block, dict) and block.get("type") not in (None, "text") for block in last_content @@ -276,13 +283,13 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM): # not deep, to avoid cloning large base64 media on the request path. payload["content"] = list(last_content) - if self._is_flag_enabled(optional_params, litellm_params, "forward_tools"): - tools: Final = optional_params.get("tools") + if self._is_flag_enabled(typed_optional_params, typed_litellm_params, "forward_tools"): + tools: Final = typed_optional_params.get("tools") if isinstance(tools, list) and tools: # Immutable shallow copy: the payload can never alias the caller's list, # the nested dicts stay shared to avoid a deep clone, and json.dumps # serializes a tuple to the same JSON array a list would. - payload["tools"] = tuple(tools) + payload["tools"] = _TOOLS.validate_python(tools) # Get or generate session ID - this goes in the header runtime_session_id: Final = self._get_runtime_session_id(optional_params) diff --git a/tests/unit/llms/bedrock/chat/agentcore/test_agentcore_transformation.py b/tests/unit/llms/bedrock/chat/agentcore/test_agentcore_transformation.py index e8f3b327252..8e34ea464e3 100644 --- a/tests/unit/llms/bedrock/chat/agentcore/test_agentcore_transformation.py +++ b/tests/unit/llms/bedrock/chat/agentcore/test_agentcore_transformation.py @@ -878,7 +878,7 @@ class TestAgentCoreForwardToolsEndToEnd: """ def test_tools_dropped_without_allowed_openai_params(self): - optional_params = litellm.utils.get_optional_params( + optional_params: Final[dict[str, object]] = litellm.utils.get_optional_params( model=AGENTCORE_TEST_MODEL, custom_llm_provider="bedrock", tools=[WEB_SEARCH_TOOL], @@ -888,7 +888,7 @@ class TestAgentCoreForwardToolsEndToEnd: assert "tools" not in optional_params def test_allowed_openai_params_carries_tools_to_the_transform(self): - optional_params = litellm.utils.get_optional_params( + optional_params: Final[dict[str, object]] = litellm.utils.get_optional_params( model=AGENTCORE_TEST_MODEL, custom_llm_provider="bedrock", tools=[WEB_SEARCH_TOOL], @@ -914,7 +914,7 @@ class TestAgentCoreForwardToolsEndToEnd: forward_tools, so this is the path every real caller takes: both the flag and the tools list arrive in optional_params and litellm_params stays empty. """ - optional_params = litellm.utils.get_optional_params( + optional_params: Final[dict[str, object]] = litellm.utils.get_optional_params( model=AGENTCORE_TEST_MODEL, custom_llm_provider="bedrock", tools=[WEB_SEARCH_TOOL], @@ -936,7 +936,7 @@ class TestAgentCoreForwardToolsEndToEnd: def test_allowed_openai_params_alone_does_not_forward(self): """Both opt-ins are required; the escape hatch alone changes nothing.""" - optional_params = litellm.utils.get_optional_params( + optional_params: Final[dict[str, object]] = litellm.utils.get_optional_params( model=AGENTCORE_TEST_MODEL, custom_llm_provider="bedrock", tools=[WEB_SEARCH_TOOL],