fix(bedrock): extract system messages from messages list for Claude messages API

This commit is contained in:
Charbel Toumieh 2026-06-05 09:01:55 +01:00
parent 8259d6cd85
commit 1ce773bd92
2 changed files with 131 additions and 0 deletions

View file

@ -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 ###
#########################################################

View file

@ -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"}]