fix(bedrock_mantle): normalize Codex agent message inputs

This commit is contained in:
Luis Gallego Ledesma 2026-08-11 12:46:45 +02:00
parent b0fac57fe4
commit be9ae8f3ab
2 changed files with 184 additions and 1 deletions

View file

@ -49,6 +49,9 @@ _BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES = frozenset({"function", "mcp", "c
_BEDROCK_MANTLE_SUPPORTED_SERVICE_TIERS: Final = frozenset({"auto", "default"})
_CODEX_ADDITIONAL_TOOLS_INPUT_ITEM_TYPE: Final = "additional_tools"
_CODEX_AGENT_MESSAGE_INPUT_ITEM_TYPE: Final = "agent_message"
_CODEX_AGENT_MESSAGE_INPUT_TEXT_CONTENT_TYPE: Final = "input_text"
_CODEX_AGENT_MESSAGE_ENCRYPTED_CONTENT_TYPE: Final = "encrypted_content"
class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPIConfig):
@ -155,6 +158,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
headers: dict,
) -> dict:
remaining_input, hoisted_tools = self._hoist_codex_additional_tools(input)
normalized_input: Final = self._normalize_codex_agent_messages(remaining_input)
request_params: Final = (
{
**response_api_optional_request_params,
@ -168,7 +172,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
)
return super().transform_responses_api_request(
model=model,
input=remaining_input,
input=normalized_input,
response_api_optional_request_params=request_params,
litellm_params=litellm_params,
headers=headers,
@ -210,6 +214,74 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
)
return remaining_input, cls._filter_unsupported_tools(hoisted_tools)
@staticmethod
def _is_codex_agent_message_item(item: object) -> bool:
return isinstance(item, dict) and item.get("type") == _CODEX_AGENT_MESSAGE_INPUT_ITEM_TYPE
@staticmethod
def _encrypted_content_block_count(item: "dict[str, Any]") -> int:
content: Final = item.get("content")
if not isinstance(content, list):
return 0
return sum(
isinstance(block, dict) and block.get("type") == _CODEX_AGENT_MESSAGE_ENCRYPTED_CONTENT_TYPE
for block in content
)
@staticmethod
def _normalize_codex_agent_message_item(item: "dict[str, Any]") -> "dict[str, Any] | None":
content: Final = item.get("content")
if not isinstance(content, list):
return None
text_parts: Final = [
block["text"]
for block in content
if isinstance(block, dict)
and block.get("type") == _CODEX_AGENT_MESSAGE_INPUT_TEXT_CONTENT_TYPE
and isinstance(block.get("text"), str)
]
if not text_parts:
return None
return {
"type": "message",
"role": "assistant",
"content": [{"type": _CODEX_AGENT_MESSAGE_INPUT_TEXT_CONTENT_TYPE, "text": "\n".join(text_parts)}],
}
@classmethod
def _normalize_codex_agent_messages(
cls,
input: "str | ResponseInputParam",
) -> "str | ResponseInputParam":
if not isinstance(input, list):
return input
agent_message_items: Final = [item for item in input if cls._is_codex_agent_message_item(item)]
if not agent_message_items:
return input
normalized_items: Final = tuple(
cls._normalize_codex_agent_message_item(item) if cls._is_codex_agent_message_item(item) else item
for item in input
)
normalized_agent_message_items: Final = tuple(
cls._normalize_codex_agent_message_item(item) for item in agent_message_items
)
normalized_input: Final = [item for item in normalized_items if item is not None]
dropped_item_count: Final = sum(item is None for item in normalized_agent_message_items)
encrypted_content_block_count: Final = sum(
cls._encrypted_content_block_count(item) for item in agent_message_items
)
verbose_logger.warning(
"Bedrock Mantle Responses API: converted %d unsupported Codex 'agent_message' item(s) to "
"'message'; dropped %d item(s) without readable text and %d encrypted_content block(s) Mantle "
"cannot process.",
len(agent_message_items) - dropped_item_count,
dropped_item_count,
encrypted_content_block_count,
)
return normalized_input
def map_openai_params(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,

View file

@ -632,6 +632,117 @@ class TestBedrockMantleCodexAdditionalTools:
assert "additional_tools" in str(mock_debug.call_args)
class TestBedrockMantleCodexAgentMessages:
_USER_MESSAGE = {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Continue the task."}],
}
def _transform(self, input):
cfg = BedrockMantleResponsesAPIConfig()
return cfg.transform_responses_api_request(
model="openai.gpt-5.6-terra",
input=input,
response_api_optional_request_params={},
litellm_params=GenericLiteLLMParams(),
headers={},
)
def test_plain_agent_message_is_normalized_to_assistant_message(self):
body = self._transform(
input=[
self._USER_MESSAGE,
{
"type": "agent_message",
"id": "amsg_123",
"author": "worker",
"recipient": "parent",
"content": [{"type": "input_text", "text": "The subtask is complete."}],
},
]
)
assert body["input"] == [
self._USER_MESSAGE,
{
"type": "message",
"role": "assistant",
"content": [{"type": "input_text", "text": "The subtask is complete."}],
},
]
def test_encrypted_agent_message_keeps_readable_text_only(self):
body = self._transform(
input=[
{
"type": "agent_message",
"author": "worker",
"recipient": "parent",
"content": [
{"type": "input_text", "text": "Message Type: MESSAGE\nSender: worker\nPayload:\n"},
{"type": "encrypted_content", "encrypted_content": "opaque-payload"},
],
}
]
)
assert body["input"] == [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "input_text", "text": "Message Type: MESSAGE\nSender: worker\nPayload:\n"}
],
}
]
def test_agent_message_without_readable_text_is_removed(self):
body = self._transform(
input=[
self._USER_MESSAGE,
{
"type": "agent_message",
"author": "worker",
"recipient": "parent",
"content": [{"type": "encrypted_content", "encrypted_content": "opaque-payload"}],
},
]
)
assert body["input"] == [self._USER_MESSAGE]
def test_agent_message_normalization_is_logged_at_warning_level(self):
from unittest.mock import patch
with patch(
"litellm.llms.bedrock_mantle.responses.transformation.verbose_logger.warning"
) as mock_warning:
self._transform(
input=[
{
"type": "agent_message",
"content": [{"type": "input_text", "text": "Done."}],
}
]
)
assert mock_warning.call_count == 1
assert "agent_message" in str(mock_warning.call_args)
def test_non_agent_items_remain_unchanged(self):
input_items = [
self._USER_MESSAGE,
{"type": "reasoning", "summary": [], "encrypted_content": "gAAAA=="},
{"type": "function_call", "name": "wait", "arguments": "{}", "call_id": "call_1"},
{"type": "function_call_output", "call_id": "call_1", "output": "done"},
]
body = self._transform(input=input_items)
assert body["input"] == input_items
class TestBedrockMantleResponsesRegistry:
def test_registry_returns_config_for_gpt_5_5(self, local_cost_map):
# gpt-5.x advertises /v1/responses in supported_endpoints (capability)