@@ -215,7 +215,6 @@ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
215215 assert is_error == False
216216
217217
218-
219218@pytest .mark .asyncio
220219async def test_mcp_tool_error ():
221220 if not _is_mcp_imported :
@@ -270,3 +269,96 @@ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
270269 ),
271270 )
272271 assert is_error == True
272+
273+
274+ def _get_weather (city : str ) -> str :
275+ return {'Paris' : 'rain' , 'Rome' : 'sun' }[city ]
276+
277+
278+ def _fail (city : str ) -> str :
279+ del city # Unused.
280+ raise ValueError ('boom' )
281+
282+
283+ def _response_with_calls (
284+ * function_calls : FunctionCall ,
285+ ) -> GenerateContentResponse :
286+ return GenerateContentResponse (
287+ candidates = [
288+ Candidate (
289+ content = Content (
290+ parts = [Part (function_call = fc ) for fc in function_calls ]
291+ )
292+ )
293+ ]
294+ )
295+
296+
297+ _PARALLEL_WEATHER_CALLS = _response_with_calls (
298+ FunctionCall (id = 'call_1' , name = 'get_weather' , args = {'city' : 'Paris' }),
299+ FunctionCall (id = 'call_2' , name = 'get_weather' , args = {'city' : 'Rome' }),
300+ )
301+
302+
303+ def test_function_call_id_propagated ():
304+ """Each response carries the id of the call it answers."""
305+ parts = get_function_response_parts (
306+ _PARALLEL_WEATHER_CALLS , {'get_weather' : _get_weather }
307+ )
308+
309+ assert [p .function_response .id for p in parts ] == ['call_1' , 'call_2' ]
310+ assert [p .function_response .response for p in parts ] == [
311+ {'result' : 'rain' },
312+ {'result' : 'sun' },
313+ ]
314+
315+
316+ @pytest .mark .asyncio
317+ async def test_function_call_id_propagated_async ():
318+ """Each async response carries the id of the call it answers."""
319+ parts = await get_function_response_parts_async (
320+ _PARALLEL_WEATHER_CALLS , {'get_weather' : _get_weather }
321+ )
322+
323+ assert [p .function_response .id for p in parts ] == ['call_1' , 'call_2' ]
324+ assert [p .function_response .response for p in parts ] == [
325+ {'result' : 'rain' },
326+ {'result' : 'sun' },
327+ ]
328+
329+
330+ def test_function_call_id_propagated_on_error ():
331+ """An error response from a failing tool still carries the call id."""
332+ response = _response_with_calls (
333+ FunctionCall (id = 'call_1' , name = 'fail' , args = {'city' : 'Paris' })
334+ )
335+
336+ parts = get_function_response_parts (response , {'fail' : _fail })
337+
338+ assert parts [0 ].function_response .id == 'call_1'
339+ assert 'boom' in parts [0 ].function_response .response ['error' ]
340+
341+
342+ @pytest .mark .asyncio
343+ async def test_function_call_id_propagated_on_error_async ():
344+ """An async error response from a failing tool still carries the call id."""
345+ response = _response_with_calls (
346+ FunctionCall (id = 'call_1' , name = 'fail' , args = {'city' : 'Paris' })
347+ )
348+
349+ parts = await get_function_response_parts_async (response , {'fail' : _fail })
350+
351+ assert parts [0 ].function_response .id == 'call_1'
352+ assert 'boom' in parts [0 ].function_response .response ['error' ]
353+
354+
355+ def test_function_call_id_absent ():
356+ """A call without an id yields a response with no id on the wire."""
357+ response = _response_with_calls (
358+ FunctionCall (name = 'get_weather' , args = {'city' : 'Paris' })
359+ )
360+
361+ parts = get_function_response_parts (response , {'get_weather' : _get_weather })
362+
363+ assert parts [0 ].function_response .id is None
364+ assert 'id' not in parts [0 ].model_dump (exclude_none = True )['function_response' ]
0 commit comments