diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 7936b6ea644..66929a3e3c5 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -2,6 +2,14 @@ from typing import TYPE_CHECKING, Any, List, Optional import httpx +from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers +from litellm.litellm_core_utils.prompt_templates.factory import ( + convert_to_anthropic_image_obj, +) +from litellm.litellm_core_utils.prompt_templates.image_handling import ( + async_convert_url_to_base64, + convert_url_to_base64, +) from litellm.llms.anthropic.chat.transformation import AnthropicConfig from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import ( AmazonInvokeConfig, @@ -113,6 +121,8 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): if "anthropic_version" not in _anthropic_request: _anthropic_request["anthropic_version"] = self.anthropic_version + self._convert_document_url_sources_to_base64(_anthropic_request) + # Remove `custom` field from tools (Bedrock doesn't support it) # Claude Code sends `custom: {defer_loading: true}` on tool definitions, # which causes Bedrock to reject the request with "Extra inputs are not permitted" @@ -144,11 +154,163 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): # Filter out beta headers that Bedrock Invoke doesn't support # Uses centralized configuration from anthropic_beta_headers_config.json - beta_list = list(beta_set) - _anthropic_request["anthropic_beta"] = beta_list + beta_list = filter_and_transform_beta_headers( + beta_headers=list(beta_set), + provider="bedrock", + ) + if beta_list: + _anthropic_request["anthropic_beta"] = beta_list return _anthropic_request + async def async_transform_request( + self, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: + # Filter out AWS authentication parameters before passing to Anthropic transformation + filtered_params = { + k: v + for k, v in optional_params.items() + if k not in self.aws_authentication_params + } + filtered_params = self._normalize_bedrock_tool_search_tools(filtered_params) + + _anthropic_request = AnthropicConfig.transform_request( + self, + model=model, + messages=messages, + optional_params=filtered_params, + litellm_params=litellm_params, + headers=headers, + ) + + _anthropic_request.pop("model", None) + _anthropic_request.pop("stream", None) + _anthropic_request.pop("output_format", None) + _anthropic_request.pop("output_config", None) + if "anthropic_version" not in _anthropic_request: + _anthropic_request["anthropic_version"] = self.anthropic_version + + await self._async_convert_document_url_sources_to_base64(_anthropic_request) + + remove_custom_field_from_tools(_anthropic_request) + + tools = optional_params.get("tools") + tool_search_used = self.is_tool_search_used(tools) + programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools) + input_examples_used = self.is_input_examples_used(tools) + + beta_set = set(get_anthropic_beta_from_headers(headers)) + auto_betas = self.get_anthropic_beta_list( + model=model, + optional_params=optional_params, + computer_tool_used=self.is_computer_tool_used(tools), + prompt_caching_set=False, + file_id_used=self.is_file_id_used(messages), + mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), + ) + beta_set.update(auto_betas) + + if tool_search_used and not ( + programmatic_tool_calling_used or input_examples_used + ): + beta_set.discard(ANTHROPIC_TOOL_SEARCH_BETA_HEADER) + if "opus-4" in model.lower() or "opus_4" in model.lower(): + beta_set.add("tool-search-tool-2025-10-19") + + beta_list = filter_and_transform_beta_headers( + beta_headers=list(beta_set), + provider="bedrock", + ) + if beta_list: + _anthropic_request["anthropic_beta"] = beta_list + + return _anthropic_request + + def _convert_document_url_sources_to_base64(self, anthropic_request: dict) -> None: + """ + Bedrock Invoke does not accept document URL sources. Convert to base64 payloads. + """ + messages = anthropic_request.get("messages") + if not isinstance(messages, list): + return + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if not isinstance(content, list): + continue + + for block in content: + if not isinstance(block, dict) or block.get("type") != "document": + continue + source = block.get("source") + if not isinstance(source, dict) or source.get("type") != "url": + continue + source_url = source.get("url") + if not isinstance(source_url, str): + continue + + inferred_format: Optional[str] = None + if source_url.lower().endswith(".pdf"): + inferred_format = "application/pdf" + base64_url = convert_url_to_base64(url=source_url) + image_chunk = convert_to_anthropic_image_obj( + openai_image_url=base64_url, + format=inferred_format, + ) + block["source"] = { + "type": "base64", + "media_type": image_chunk["media_type"], + "data": image_chunk["data"], + } + + async def _async_convert_document_url_sources_to_base64( + self, anthropic_request: dict + ) -> None: + """ + Async version of document URL conversion for async completion paths. + """ + messages = anthropic_request.get("messages") + if not isinstance(messages, list): + return + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if not isinstance(content, list): + continue + + for block in content: + if not isinstance(block, dict) or block.get("type") != "document": + continue + source = block.get("source") + if not isinstance(source, dict) or source.get("type") != "url": + continue + source_url = source.get("url") + if not isinstance(source_url, str): + continue + + inferred_format: Optional[str] = None + if source_url.lower().endswith(".pdf"): + inferred_format = "application/pdf" + base64_url = await async_convert_url_to_base64(url=source_url) + image_chunk = convert_to_anthropic_image_obj( + openai_image_url=base64_url, + format=inferred_format, + ) + block["source"] = { + "type": "base64", + "media_type": image_chunk["media_type"], + "data": image_chunk["data"], + } + def _normalize_bedrock_tool_search_tools(self, optional_params: dict) -> dict: """ Convert tool search entries to the format supported by the Bedrock Invoke API.