From 1ce773bd92cb5eff992d0cddf5e504b2c79eb8b6 Mon Sep 17 00:00:00 2001 From: Charbel Toumieh Date: Fri, 5 Jun 2026 09:01:55 +0100 Subject: [PATCH] fix(bedrock): extract system messages from messages list for Claude messages API --- .../anthropic_claude3_transformation.py | 50 ++++++++++++ .../test_bedrock_anthropic_messages_test.py | 81 +++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index 42c3bd517a9..3587aa8042f 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -564,6 +564,53 @@ class AmazonAnthropicClaudeMessagesConfig( normalize_bedrock_opus_output_config_effort(model=model, output_config=clamped) optional_params["reasoning_effort"] = clamped["effort"] + def _extract_system_messages_from_messages( + self, anthropic_messages_request: Dict + ) -> None: + """ + Extract system messages from the messages array and merge them into the system parameter. + """ + system_messages_to_add = [] + messages_list = anthropic_messages_request.get("messages", []) + new_messages = [] + system_messages_found = False + for msg in messages_list: + if isinstance(msg, dict) and msg.get("role") == "system": + system_messages_found = True + content = msg.get("content") + if isinstance(content, str): + if content: + block = {"type": "text", "text": content} + if "cache_control" in msg: + block["cache_control"] = msg["cache_control"] + system_messages_to_add.append(block) + elif isinstance(content, list): + for item in content: + if isinstance(item, dict): + if item.get("type") == "text": + text_val = item.get("text") + if text_val: + block = {"type": "text", "text": text_val} + if "cache_control" in item: + block["cache_control"] = item["cache_control"] + system_messages_to_add.append(block) + else: + new_messages.append(msg) + + if system_messages_found: + anthropic_messages_request["messages"] = new_messages + if system_messages_to_add: + existing_system = anthropic_messages_request.get("system") + if existing_system is None: + anthropic_messages_request["system"] = system_messages_to_add + elif isinstance(existing_system, str): + if existing_system: + anthropic_messages_request["system"] = [{"type": "text", "text": existing_system}] + system_messages_to_add + else: + anthropic_messages_request["system"] = system_messages_to_add + elif isinstance(existing_system, list): + anthropic_messages_request["system"] = list(existing_system) + system_messages_to_add + def transform_anthropic_messages_request( self, model: str, @@ -584,6 +631,9 @@ class AmazonAnthropicClaudeMessagesConfig( litellm_params=litellm_params, headers=headers, ) + + # Extract system messages from messages list (Bedrock does not support system role in messages) + self._extract_system_messages_from_messages(anthropic_messages_request) ######################################################### ############## BEDROCK Invoke SPECIFIC TRANSFORMATION ### ######################################################### diff --git a/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py b/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py index dcc44cae77e..0ec468b5d8a 100644 --- a/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py +++ b/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py @@ -136,3 +136,84 @@ async def test_should_not_fail_with_forwarded_headers_bedrock_invoke_messages(): print("INVOKE API RESPONSE: ", response) INSTANCE_BASE_ANTHROPIC_MESSAGES_TEST._validate_response(response) + + +def test_bedrock_anthropic_messages_system_role_transformation(): + """ + Test that system messages in the messages array are correctly extracted and moved + to the top-level system parameter for Bedrock. + """ + config = litellm.AmazonAnthropicClaudeMessagesConfig() + + # 1. Test case: System message in messages array, no existing system param + req = config.transform_anthropic_messages_request( + model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=[ + {"role": "system", "content": "You are a helpful assistant"}, + {"role": "user", "content": "Hello"} + ], + anthropic_messages_optional_request_params={"max_tokens": 100}, + litellm_params={}, + headers={} + ) + + assert req["messages"] == [{"role": "user", "content": "Hello"}] + assert req["system"] == [{"type": "text", "text": "You are a helpful assistant"}] + + # 2. Test case: System message in messages array, and existing system param + req = config.transform_anthropic_messages_request( + model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=[ + {"role": "system", "content": "System message in messages array"}, + {"role": "user", "content": "Hello"} + ], + anthropic_messages_optional_request_params={ + "max_tokens": 100, + "system": "Existing top-level system message" + }, + litellm_params={}, + headers={} + ) + + assert req["messages"] == [{"role": "user", "content": "Hello"}] + assert req["system"] == [ + {"type": "text", "text": "Existing top-level system message"}, + {"type": "text", "text": "System message in messages array"} + ] + + # 3. Test case: Empty system message content (should remove system message from messages, system param unaffected) + req = config.transform_anthropic_messages_request( + model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=[ + {"role": "system", "content": ""}, + {"role": "user", "content": "Hello"} + ], + anthropic_messages_optional_request_params={"max_tokens": 100}, + litellm_params={}, + headers={} + ) + + assert req["messages"] == [{"role": "user", "content": "Hello"}] + assert "system" not in req + + # 4. Test case: Non-text items in list content (should only extract text items, ignore non-text) + req = config.transform_anthropic_messages_request( + model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=[ + { + "role": "system", + "content": [ + {"type": "text", "text": "This is text"}, + {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "abc"}} + ] + }, + {"role": "user", "content": "Hello"} + ], + anthropic_messages_optional_request_params={"max_tokens": 100}, + litellm_params={}, + headers={} + ) + + assert req["messages"] == [{"role": "user", "content": "Hello"}] + assert req["system"] == [{"type": "text", "text": "This is text"}] +