fix: ignore empty function chunk in LiteLlm streaming response

Fixes https://github.com/google/adk-python/issues/1532

PiperOrigin-RevId: 808636127
This commit is contained in:
Xuan Yang
2025-09-18 10:18:22 -07:00
committed by Copybara-Service
parent c37bd2742c
commit 8a92fd18b6
2 changed files with 108 additions and 2 deletions
+9 -2
View File
@@ -437,10 +437,17 @@ def _model_response_to_chunk(
for tool_call in message.get("tool_calls"):
# aggregate tool_call
if tool_call.type == "function":
func_name = tool_call.function.name
func_args = tool_call.function.arguments
# Ignore empty chunks that don't carry any information.
if not func_name and not func_args:
continue
yield FunctionChunk(
id=tool_call.id,
name=tool_call.function.name,
args=tool_call.function.arguments,
name=func_name,
args=func_args,
index=tool_call.index,
), finish_reason
+99
View File
@@ -267,6 +267,77 @@ MULTIPLE_FUNCTION_CALLS_STREAM = [
]
STREAM_WITH_EMPTY_CHUNK = [
ModelResponse(
choices=[
StreamingChoices(
finish_reason=None,
delta=Delta(
role="assistant",
tool_calls=[
ChatCompletionDeltaToolCall(
type="function",
id="call_abc",
function=Function(
name="test_function",
arguments='{"test_arg":',
),
index=0,
)
],
),
)
]
),
ModelResponse(
choices=[
StreamingChoices(
finish_reason=None,
delta=Delta(
role="assistant",
tool_calls=[
ChatCompletionDeltaToolCall(
type="function",
id=None,
function=Function(
name=None,
arguments=' "value"}',
),
index=0,
)
],
),
)
]
),
# This is the problematic empty chunk that should be ignored.
ModelResponse(
choices=[
StreamingChoices(
finish_reason=None,
delta=Delta(
role="assistant",
tool_calls=[
ChatCompletionDeltaToolCall(
type="function",
id=None,
function=Function(
name=None,
arguments="",
),
index=0,
)
],
),
)
]
),
ModelResponse(
choices=[StreamingChoices(finish_reason="tool_calls", delta=Delta())]
),
]
@pytest.fixture
def mock_response():
return ModelResponse(
@@ -1591,6 +1662,34 @@ async def test_generate_content_async_non_compliant_multiple_function_calls(
assert final_response.content.parts[1].function_call.args == {"arg": "value2"}
@pytest.mark.asyncio
async def test_generate_content_async_stream_with_empty_chunk(
mock_completion, lite_llm_instance
):
"""Tests that empty tool call chunks in a stream are ignored."""
mock_completion.return_value = iter(STREAM_WITH_EMPTY_CHUNK)
responses = [
response
async for response in lite_llm_instance.generate_content_async(
LLM_REQUEST_WITH_FUNCTION_DECLARATION, stream=True
)
]
assert len(responses) == 1
final_response = responses[0]
assert final_response.content.role == "model"
# Crucially, assert that only ONE tool call was generated,
# proving the empty chunk was ignored.
assert len(final_response.content.parts) == 1
function_call = final_response.content.parts[0].function_call
assert function_call.name == "test_function"
assert function_call.id == "call_abc"
assert function_call.args == {"test_arg": "value"}
@pytest.mark.asyncio
def test_get_completion_inputs_generation_params():
# Test that generation_params are extracted and mapped correctly