@@ -115,6 +115,38 @@ def __init__(
115115
116116 self .client = AsyncOpenAI (** client_args )
117117
118+ @staticmethod
119+ def _normalize_and_validate_messages (
120+ messages : list [dict | ChatMessage ],
121+ provider_name : str ,
122+ ) -> list [dict ]:
123+ """Convert ``ChatMessage`` objects and validate OpenAI message shape."""
124+ if not isinstance (messages , list ):
125+ raise ValueError (
126+ f"{ provider_name } `messages` field expected type `list`, " f"got `{ type (messages )} ` instead." ,
127+ )
128+
129+ normalized_messages = [msg .to_dict () if isinstance (msg , ChatMessage ) else msg for msg in messages ]
130+
131+ def _is_valid_message (msg : dict ) -> bool :
132+ if not isinstance (msg , dict ) or "role" not in msg :
133+ return False
134+ role = msg ["role" ]
135+ if role == "assistant" and ("tool_calls" in msg or "function_call" in msg ):
136+ return True
137+ if role == "tool" :
138+ return "tool_call_id" in msg and "content" in msg
139+ return "content" in msg
140+
141+ if not all (_is_valid_message (msg ) for msg in normalized_messages ):
142+ raise ValueError (
143+ "Invalid message format. Each message must have 'role' and appropriate fields. "
144+ "User/system messages need 'content'. Tool messages need 'tool_call_id' and 'content'. "
145+ "Assistant messages with 'tool_calls' or 'function_call' don't require 'content'." ,
146+ )
147+
148+ return normalized_messages
149+
118150 async def achat (
119151 self ,
120152 messages : list [dict | ChatMessage ],
@@ -164,34 +196,7 @@ async def achat(
164196 The response from the OpenAI chat completions API.
165197 """
166198
167- # checking messages
168- if not isinstance (messages , list ):
169- raise ValueError (
170- "OpenAI `messages` field expected type `list`, " f"got `{ type (messages )} ` instead." ,
171- )
172- messages = [msg .to_dict () if isinstance (msg , ChatMessage ) else msg for msg in messages ]
173-
174- # Validate messages - note that for assistant messages with tool_calls,
175- # content can be None or missing (this is valid OpenAI format)
176- def _is_valid_message (msg : dict ) -> bool :
177- if not isinstance (msg , dict ) or "role" not in msg :
178- return False
179- role = msg ["role" ]
180- # Assistant messages with tool_calls don't require content
181- if role == "assistant" and "tool_calls" in msg :
182- return True
183- # Tool messages require tool_call_id and content
184- if role == "tool" :
185- return "tool_call_id" in msg and "content" in msg
186- # All other messages require content
187- return "content" in msg
188-
189- if not all (_is_valid_message (msg ) for msg in messages ):
190- raise ValueError (
191- "Invalid message format. Each message must have 'role' and appropriate fields. "
192- "User/system messages need 'content'. Tool messages need 'tool_call_id' and 'content'. "
193- "Assistant messages with 'tool_calls' don't require 'content'." ,
194- )
199+ messages = self ._normalize_and_validate_messages (messages , "OpenAI" )
195200
196201 # Qwen-omni requires different base64 audio format from openai
197202 if "omni" in self .model .lower ():
0 commit comments