@@ -290,6 +290,105 @@ def mock_response():
290
290
)
291
291
292
292
293
+ # Test case reflecting litellm v1.71.2, ollama v0.9.0 streaming response
294
+ # no tool call ids
295
+ # indices all 0
296
+ # finish_reason stop instead of tool_calls
297
+ NON_COMPLIANT_MULTIPLE_FUNCTION_CALLS_STREAM = [
298
+ ModelResponse (
299
+ choices = [
300
+ StreamingChoices (
301
+ finish_reason = None ,
302
+ delta = Delta (
303
+ role = "assistant" ,
304
+ tool_calls = [
305
+ ChatCompletionDeltaToolCall (
306
+ type = "function" ,
307
+ id = None ,
308
+ function = Function (
309
+ name = "function_1" ,
310
+ arguments = '{"arg": "val' ,
311
+ ),
312
+ index = 0 ,
313
+ )
314
+ ],
315
+ ),
316
+ )
317
+ ]
318
+ ),
319
+ ModelResponse (
320
+ choices = [
321
+ StreamingChoices (
322
+ finish_reason = None ,
323
+ delta = Delta (
324
+ role = "assistant" ,
325
+ tool_calls = [
326
+ ChatCompletionDeltaToolCall (
327
+ type = "function" ,
328
+ id = None ,
329
+ function = Function (
330
+ name = None ,
331
+ arguments = 'ue1"}' ,
332
+ ),
333
+ index = 0 ,
334
+ )
335
+ ],
336
+ ),
337
+ )
338
+ ]
339
+ ),
340
+ ModelResponse (
341
+ choices = [
342
+ StreamingChoices (
343
+ finish_reason = None ,
344
+ delta = Delta (
345
+ role = "assistant" ,
346
+ tool_calls = [
347
+ ChatCompletionDeltaToolCall (
348
+ type = "function" ,
349
+ id = None ,
350
+ function = Function (
351
+ name = "function_2" ,
352
+ arguments = '{"arg": "val' ,
353
+ ),
354
+ index = 0 ,
355
+ )
356
+ ],
357
+ ),
358
+ )
359
+ ]
360
+ ),
361
+ ModelResponse (
362
+ choices = [
363
+ StreamingChoices (
364
+ finish_reason = None ,
365
+ delta = Delta (
366
+ role = "assistant" ,
367
+ tool_calls = [
368
+ ChatCompletionDeltaToolCall (
369
+ type = "function" ,
370
+ id = None ,
371
+ function = Function (
372
+ name = None ,
373
+ arguments = 'ue2"}' ,
374
+ ),
375
+ index = 0 ,
376
+ )
377
+ ],
378
+ ),
379
+ )
380
+ ]
381
+ ),
382
+ ModelResponse (
383
+ choices = [
384
+ StreamingChoices (
385
+ finish_reason = "stop" ,
386
+ )
387
+ ]
388
+ ),
389
+ ]
390
+
391
+
293
392
@pytest .fixture
294
393
def mock_acompletion (mock_response ):
295
394
return AsyncMock (return_value = mock_response )
@@ -1257,3 +1356,76 @@ async def test_generate_content_async_multiple_function_calls(
1257
1356
assert final_response .content .parts [1 ].function_call .name == "function_2"
1258
1357
assert final_response .content .parts [1 ].function_call .id == "call_2"
1259
1358
assert final_response .content .parts [1 ].function_call .args == {"arg" : "value2" }
1359
+
1360
+
1361
+ @pytest .mark .asyncio
1362
+ async def test_generate_content_async_non_compliant_multiple_function_calls (
1363
+ mock_completion , lite_llm_instance
1364
+ ):
1365
+ """Test handling of multiple function calls with same 0 indices in streaming mode.
1366
+
1367
+ This test verifies that:
1368
+ 1. Multiple function calls with same indices (0) are handled correctly
1369
+ 2. Arguments and names are properly accumulated for each function call
1370
+ 3. The final response contains all function calls with correct incremented indices
1371
+ """
1372
+ mock_completion .return_value = NON_COMPLIANT_MULTIPLE_FUNCTION_CALLS_STREAM
1373
+
1374
+ llm_request = LlmRequest (
1375
+ contents = [
1376
+ types .Content (
1377
+ role = "user" ,
1378
+ parts = [types .Part .from_text (text = "Test multiple function calls" )],
1379
+ )
1380
+ ],
1381
+ config = types .GenerateContentConfig (
1382
+ tools = [
1383
+ types .Tool (
1384
+ function_declarations = [
1385
+ types .FunctionDeclaration (
1386
+ name = "function_1" ,
1387
+ description = "First test function" ,
1388
+ parameters = types .Schema (
1389
+ type = types .Type .OBJECT ,
1390
+ properties = {
1391
+ "arg" : types .Schema (type = types .Type .STRING ),
1392
+ },
1393
+ ),
1394
+ ),
1395
+ types .FunctionDeclaration (
1396
+ name = "function_2" ,
1397
+ description = "Second test function" ,
1398
+ parameters = types .Schema (
1399
+ type = types .Type .OBJECT ,
1400
+ properties = {
1401
+ "arg" : types .Schema (type = types .Type .STRING ),
1402
+ },
1403
+ ),
1404
+ ),
1405
+ ]
1406
+ )
1407
+ ],
1408
+ ),
1409
+ )
1410
+
1411
+ responses = []
1412
+ async for response in lite_llm_instance .generate_content_async (
1413
+ llm_request , stream = True
1414
+ ):
1415
+ responses .append (response )
1416
+
1417
+ # Verify we got the final response with both function calls
1418
+ assert len (responses ) > 0
1419
+ final_response = responses [- 1 ]
1420
+ assert final_response .content .role == "model"
1421
+ assert len (final_response .content .parts ) == 2
1422
+
1423
+ # Verify first function call
1424
+ assert final_response .content .parts [0 ].function_call .name == "function_1"
1425
+ assert final_response .content .parts [0 ].function_call .id == "0"
1426
+ assert final_response .content .parts [0 ].function_call .args == {"arg" : "value1" }
1427
+
1428
+ # Verify second function call
1429
+ assert final_response .content .parts [1 ].function_call .name == "function_2"
1430
+ assert final_response .content .parts [1 ].function_call .id == "1"
1431
+ assert final_response .content .parts [1 ].function_call .args == {"arg" : "value2" }
0 commit comments