@@ -664,15 +664,43 @@ async def _call_sub_tool(
664664 "tool_call_id" : tool_call_id ,
665665 }
666666
667+ def _normalize_message_content (self , messages : list [dict ]) -> list [dict ]:
668+ """Normalize message content fields to formats the API accepts.
669+
670+ The API expects content to be: string, array of objects, or None.
671+ Handles several malformed cases:
672+ 1. Content is a nested message dict (has 'role' and 'content' keys) - extract inner content
673+ 2. Content is a content part object (has 'type' key) - wrap in array
674+ """
675+ normalized = []
676+ for msg in messages :
677+ msg_copy = dict (msg )
678+ content = msg_copy .get ("content" )
679+
680+ if content is not None and isinstance (content , dict ):
681+ # Check if content is a nested message dict (has 'role' and 'content' keys)
682+ # This happens when model passes message dicts to llm_batch instead of strings
683+ if "role" in content and "content" in content :
684+ msg_copy ["content" ] = content ["content" ]
685+ elif "type" in content :
686+ # Content part object (e.g. {"type": "text", "text": "..."}) - wrap in array
687+ msg_copy ["content" ] = [content ]
688+ else :
689+ # Unknown dict structure - try wrapping in array as fallback
690+ msg_copy ["content" ] = [content ]
691+ normalized .append (msg_copy )
692+ return normalized
693+
667694 async def _call_sub_llm_api (
668695 self , client : Any , model : str , messages : list [dict ], tools : list | None = None
669696 ) -> Any | None :
670697 """Make a single sub-LLM API call with timeout. Returns None on timeout."""
698+ normalized_messages = self ._normalize_message_content (messages )
671699 try :
672700 return await asyncio .wait_for (
673701 client .chat .completions .create (
674702 model = model ,
675- messages = messages ,
703+ messages = normalized_messages ,
676704 tools = tools ,
677705 logprobs = self ._sub_llm_supports_logprobs or None ,
678706 ),
0 commit comments