mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
fix(bedrock_mantle): normalize Codex agent message inputs
This commit is contained in:
parent
b0fac57fe4
commit
be9ae8f3ab
2 changed files with 184 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue