From a34bb6f6d375c97f1db286f2644393c4d163c329 Mon Sep 17 00:00:00 2001 From: Tim Elfrink Date: Mon, 15 Sep 2025 10:58:58 +0200 Subject: [PATCH] Implement requestMetadata support for Bedrock Converse API - Add requestMetadata field to CommonRequestObject type definition - Support request_metadata parameter in get_supported_openai_params - Add comprehensive validation for AWS Bedrock constraints: * Maximum 16 key-value pairs * Key length 1-256 characters * Value length 0-256 characters * Character set validation [a-zA-Z0-9\s:_@0=/+,.-] - Transform request_metadata to top-level requestMetadata field in API request - Maintain backward compatibility with existing functionality - Enable metadata logging and traceability for multi-cloud environments --- .../bedrock/chat/converse_transformation.py | 85 +++++++++++++++++++ litellm/types/llms/bedrock.py | 3 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index d99cff6b6b..5cc644dd4e 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -175,6 +175,77 @@ class AmazonConverseConfig(BaseConfig): and v is not None } + def _validate_request_metadata(self, metadata: dict) -> None: + """ + Validate requestMetadata according to AWS Bedrock Converse API constraints. + + Constraints: + - Maximum of 16 items + - Keys: 1-256 characters, pattern [a-zA-Z0-9\\s:_@$#=/+,-.]{1,256} + - Values: 0-256 characters, pattern [a-zA-Z0-9\\s:_@$#=/+,-.]{0,256} + """ + import re + + if not isinstance(metadata, dict): + raise litellm.exceptions.BadRequestError( + message="request_metadata must be a dictionary", + model="bedrock", + llm_provider="bedrock", + ) + + if len(metadata) > 16: + raise litellm.exceptions.BadRequestError( + message="request_metadata can contain a maximum of 16 items", + model="bedrock", + llm_provider="bedrock", + ) + + key_pattern = re.compile(r'^[a-zA-Z0-9\s:_@$#=/+,.-]{1,256}$') + value_pattern = re.compile(r'^[a-zA-Z0-9\s:_@$#=/+,.-]{0,256}$') + + for key, value in metadata.items(): + if not isinstance(key, str): + raise litellm.exceptions.BadRequestError( + message="request_metadata keys must be strings", + model="bedrock", + llm_provider="bedrock", + ) + + if not isinstance(value, str): + raise litellm.exceptions.BadRequestError( + message="request_metadata values must be strings", + model="bedrock", + llm_provider="bedrock", + ) + + if len(key) == 0 or len(key) > 256: + raise litellm.exceptions.BadRequestError( + message="request_metadata key length must be 1-256 characters", + model="bedrock", + llm_provider="bedrock", + ) + + if len(value) > 256: + raise litellm.exceptions.BadRequestError( + message="request_metadata value length must be 0-256 characters", + model="bedrock", + llm_provider="bedrock", + ) + + if not key_pattern.match(key): + raise litellm.exceptions.BadRequestError( + message=f"request_metadata key '{key}' contains invalid characters. Allowed: [a-zA-Z0-9\\s:_@$#=/+,.-]", + model="bedrock", + llm_provider="bedrock", + ) + + if not value_pattern.match(value): + raise litellm.exceptions.BadRequestError( + message=f"request_metadata value '{value}' contains invalid characters. Allowed: [a-zA-Z0-9\\s:_@$#=/+,.-]", + model="bedrock", + llm_provider="bedrock", + ) + def get_supported_openai_params(self, model: str) -> List[str]: from litellm.utils import supports_function_calling @@ -188,6 +259,7 @@ class AmazonConverseConfig(BaseConfig): "top_p", "extra_headers", "response_format", + "request_metadata", ] if ( @@ -497,6 +569,10 @@ class AmazonConverseConfig(BaseConfig): optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( value ) + if param == "request_metadata": + if value is not None: + self._validate_request_metadata(value) + optional_params["request_metadata"] = value # Only update thinking tokens for non-GPT-OSS models if "gpt-oss" not in model: @@ -727,6 +803,11 @@ class AmazonConverseConfig(BaseConfig): ) inference_params.pop("json_mode", None) # used for handling json_schema + # Extract request_metadata before processing other parameters + request_metadata = inference_params.pop("request_metadata", None) + if request_metadata is not None: + self._validate_request_metadata(request_metadata) + # keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params' additional_request_params = { k: v for k, v in inference_params.items() if k not in total_supported_params @@ -813,6 +894,10 @@ class AmazonConverseConfig(BaseConfig): if bedrock_tool_config is not None: data["toolConfig"] = bedrock_tool_config + # Request Metadata (top-level field) + if request_metadata is not None: + data["requestMetadata"] = request_metadata + return data async def _async_transform_request( diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index a829a6b94b..62143e8803 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -1,5 +1,5 @@ import json -from typing import Any, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from typing_extensions import ( TYPE_CHECKING, @@ -227,6 +227,7 @@ class CommonRequestObject( toolConfig: ToolConfigBlock guardrailConfig: Optional[GuardrailConfigBlock] performanceConfig: Optional[PerformanceConfigBlock] + requestMetadata: Optional[Dict[str, str]] class RequestObject(CommonRequestObject, total=False):