@@ -141,15 +141,62 @@ def _convert_tool_call_id_to_mistral_compatible(tool_call_id: str) -> str:
141141 return base62_str .rjust (9 , "0" )
142142
143143
144+ def _extract_mistral_citations (content : Any ) -> list [dict [str , Any ]]:
145+ """Extract Mistral reference blocks from content."""
146+ if not isinstance (content , list ):
147+ return []
148+ return [
149+ {key : value for key , value in block .items () if key != "index" }
150+ for block in content
151+ if isinstance (block , dict ) and block .get ("type" ) == "reference"
152+ ]
153+
154+
155+ def _normalize_mistral_assistant_content (
156+ raw_content : Any ,
157+ ) -> tuple [str | list [str | dict ], list [dict [str , Any ]]]:
158+ """Normalize Mistral assistant content and extract citation blocks."""
159+ if not isinstance (raw_content , list ):
160+ return raw_content or "" , []
161+
162+ citations = _extract_mistral_citations (raw_content )
163+ if not citations :
164+ return cast ("list[str | dict]" , raw_content ), []
165+
166+ text_parts : list [str ] = []
167+ should_flatten = True
168+ for block in raw_content :
169+ if isinstance (block , str ):
170+ text_parts .append (block )
171+ elif isinstance (block , dict ):
172+ if block .get ("type" ) == "reference" or (
173+ block .get ("type" ) == "text" and set (block ) <= {"type" , "text" }
174+ ):
175+ text = block .get ("text" )
176+ text_parts .append (text if isinstance (text , str ) else str (text or "" ))
177+ else :
178+ should_flatten = False
179+ else :
180+ should_flatten = False
181+
182+ if should_flatten :
183+ return "" .join (text_parts ), citations
184+ return cast ("list[str | dict]" , raw_content ), citations
185+
186+
144187def _convert_mistral_chat_message_to_message (
145188 _message : dict ,
146189) -> BaseMessage :
147190 role = _message ["role" ]
148191 if role != "assistant" :
149192 msg = f"Expected role to be 'assistant', got { role } "
150193 raise ValueError (msg )
151- # Mistral returns None for tool invocations
152- content = _message .get ("content" , "" ) or ""
194+ # Mistral returns None for tool invocations. It can also return typed content
195+ # blocks for citations; keep the answer text backward compatible and surface
196+ # citation metadata separately.
197+ content , citations = _normalize_mistral_assistant_content (
198+ _message .get ("content" , "" )
199+ )
153200
154201 additional_kwargs : dict = {}
155202 tool_calls = []
@@ -166,12 +213,15 @@ def _convert_mistral_chat_message_to_message(
166213 tool_calls .append (parsed )
167214 except Exception as e :
168215 invalid_tool_calls .append (make_invalid_tool_call (raw_tool_call , str (e )))
216+ response_metadata : dict [str , Any ] = {"model_provider" : "mistralai" }
217+ if citations :
218+ response_metadata ["citations" ] = citations
169219 return AIMessage (
170220 content = content ,
171221 additional_kwargs = additional_kwargs ,
172222 tool_calls = tool_calls ,
173223 invalid_tool_calls = invalid_tool_calls ,
174- response_metadata = { "model_provider" : "mistralai" } ,
224+ response_metadata = response_metadata ,
175225 )
176226
177227
@@ -255,6 +305,7 @@ def _convert_chunk_to_message_chunk(
255305 content = _delta .get ("content" ) or ""
256306 if output_version == "v1" and isinstance (content , str ):
257307 content = [{"type" : "text" , "text" : content }]
308+ citations = _extract_mistral_citations (content )
258309 if isinstance (content , list ):
259310 for block in content :
260311 if isinstance (block , dict ):
@@ -273,7 +324,9 @@ def _convert_chunk_to_message_chunk(
273324 return HumanMessageChunk (content = content ), index , index_type
274325 if role == "assistant" or default_class == AIMessageChunk :
275326 additional_kwargs : dict = {}
276- response_metadata = {}
327+ response_metadata : dict [str , Any ] = {}
328+ if citations :
329+ response_metadata ["citations" ] = citations
277330 if raw_tool_calls := _delta .get ("tool_calls" ):
278331 additional_kwargs ["tool_calls" ] = raw_tool_calls
279332 try :
0 commit comments