|
267 | 267 | ]
|
268 | 268 |
|
269 | 269 |
|
| 270 | +STREAM_WITH_EMPTY_CHUNK = [ |
| 271 | + ModelResponse( |
| 272 | + choices=[ |
| 273 | + StreamingChoices( |
| 274 | + finish_reason=None, |
| 275 | + delta=Delta( |
| 276 | + role="assistant", |
| 277 | + tool_calls=[ |
| 278 | + ChatCompletionDeltaToolCall( |
| 279 | + type="function", |
| 280 | + id="call_abc", |
| 281 | + function=Function( |
| 282 | + name="test_function", |
| 283 | + arguments='{"test_arg":', |
| 284 | + ), |
| 285 | + index=0, |
| 286 | + ) |
| 287 | + ], |
| 288 | + ), |
| 289 | + ) |
| 290 | + ] |
| 291 | + ), |
| 292 | + ModelResponse( |
| 293 | + choices=[ |
| 294 | + StreamingChoices( |
| 295 | + finish_reason=None, |
| 296 | + delta=Delta( |
| 297 | + role="assistant", |
| 298 | + tool_calls=[ |
| 299 | + ChatCompletionDeltaToolCall( |
| 300 | + type="function", |
| 301 | + id=None, |
| 302 | + function=Function( |
| 303 | + name=None, |
| 304 | + arguments=' "value"}', |
| 305 | + ), |
| 306 | + index=0, |
| 307 | + ) |
| 308 | + ], |
| 309 | + ), |
| 310 | + ) |
| 311 | + ] |
| 312 | + ), |
| 313 | + # This is the problematic empty chunk that should be ignored. |
| 314 | + ModelResponse( |
| 315 | + choices=[ |
| 316 | + StreamingChoices( |
| 317 | + finish_reason=None, |
| 318 | + delta=Delta( |
| 319 | + role="assistant", |
| 320 | + tool_calls=[ |
| 321 | + ChatCompletionDeltaToolCall( |
| 322 | + type="function", |
| 323 | + id=None, |
| 324 | + function=Function( |
| 325 | + name=None, |
| 326 | + arguments="", |
| 327 | + ), |
| 328 | + index=0, |
| 329 | + ) |
| 330 | + ], |
| 331 | + ), |
| 332 | + ) |
| 333 | + ] |
| 334 | + ), |
| 335 | + ModelResponse( |
| 336 | + choices=[StreamingChoices(finish_reason="tool_calls", delta=Delta())] |
| 337 | + ), |
| 338 | +] |
| 339 | + |
| 340 | + |
270 | 341 | @pytest.fixture
|
271 | 342 | def mock_response():
|
272 | 343 | return ModelResponse(
|
@@ -1591,6 +1662,34 @@ async def test_generate_content_async_non_compliant_multiple_function_calls(
|
1591 | 1662 | assert final_response.content.parts[1].function_call.args == {"arg": "value2"}
|
1592 | 1663 |
|
1593 | 1664 |
|
| 1665 | +@pytest.mark.asyncio |
| 1666 | +async def test_generate_content_async_stream_with_empty_chunk( |
| 1667 | + mock_completion, lite_llm_instance |
| 1668 | +): |
| 1669 | + """Tests that empty tool call chunks in a stream are ignored.""" |
| 1670 | + mock_completion.return_value = iter(STREAM_WITH_EMPTY_CHUNK) |
| 1671 | + |
| 1672 | + responses = [ |
| 1673 | + response |
| 1674 | + async for response in lite_llm_instance.generate_content_async( |
| 1675 | + LLM_REQUEST_WITH_FUNCTION_DECLARATION, stream=True |
| 1676 | + ) |
| 1677 | + ] |
| 1678 | + |
| 1679 | + assert len(responses) == 1 |
| 1680 | + final_response = responses[0] |
| 1681 | + assert final_response.content.role == "model" |
| 1682 | + |
| 1683 | + # Crucially, assert that only ONE tool call was generated, |
| 1684 | + # proving the empty chunk was ignored. |
| 1685 | + assert len(final_response.content.parts) == 1 |
| 1686 | + |
| 1687 | + function_call = final_response.content.parts[0].function_call |
| 1688 | + assert function_call.name == "test_function" |
| 1689 | + assert function_call.id == "call_abc" |
| 1690 | + assert function_call.args == {"test_arg": "value"} |
| 1691 | + |
| 1692 | + |
1594 | 1693 | @pytest.mark.asyncio
|
1595 | 1694 | def test_get_completion_inputs_generation_params():
|
1596 | 1695 | # Test that generation_params are extracted and mapped correctly
|
|
0 commit comments