mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix: restore tool_calls finish_reason for empty-text final chunk (issue #22900)
The guard condition `choice.delta.content is None` excluded the case where
Gemini sends `parts: [{text: ""}]` (empty string, not None) as the final
chunk after tool calls. Widen to `not choice.delta.content` to cover both
None and empty-string. Also restores the test for this case that was dropped
in the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9cfe003617
commit
901144dbf8
2 changed files with 67 additions and 1 deletions
|
|
@ -3120,7 +3120,7 @@ class ModelResponseIterator:
|
|||
choice.delta is None
|
||||
or (
|
||||
not choice.delta.tool_calls
|
||||
and choice.delta.content is None
|
||||
and not choice.delta.content
|
||||
)
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -337,3 +337,69 @@ def test_streaming_tool_calls_then_empty_content_finish_reason_is_tool_calls():
|
|||
assert len(response2.choices) == 1
|
||||
assert response2.choices[0].finish_reason == "tool_calls"
|
||||
assert response2.choices[0].delta.content is None
|
||||
|
||||
|
||||
def test_streaming_tool_calls_then_empty_text_finish_reason_is_tool_calls():
|
||||
"""
|
||||
When Gemini streams tool calls in one chunk and the final chunk has BOTH
|
||||
empty content (parts: [{text: ""}]) AND finishReason="STOP", the
|
||||
finish_reason must still be "tool_calls".
|
||||
|
||||
This covers models like gemini-3.1-flash-lite-preview that send the
|
||||
final chunk with content (empty text) instead of omitting it entirely.
|
||||
|
||||
Ref: https://github.com/BerriAI/litellm/issues/22900
|
||||
"""
|
||||
logging_obj = _make_logging_obj()
|
||||
iterator = ModelResponseIterator(
|
||||
streaming_response=iter([]),
|
||||
sync_stream=True,
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
# Chunk 1: tool call with no finishReason
|
||||
chunk_with_tool_calls = {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"name": "get_current_weather",
|
||||
"args": {"location": "Boston, MA"},
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
},
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Chunk 2: finishReason="STOP" with empty text content (not None, but "")
|
||||
chunk_empty_text_with_finish = {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [{"text": ""}],
|
||||
"role": "model",
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Process chunk 1: tool calls
|
||||
response1 = iterator.chunk_parser(chunk_with_tool_calls)
|
||||
assert response1 is not None
|
||||
assert len(response1.choices) == 1
|
||||
assert response1.choices[0].delta.tool_calls is not None
|
||||
assert iterator.has_seen_tool_calls is True
|
||||
|
||||
# Process chunk 2: empty text content with finishReason
|
||||
response2 = iterator.chunk_parser(chunk_empty_text_with_finish)
|
||||
assert response2 is not None
|
||||
assert len(response2.choices) == 1
|
||||
assert response2.choices[0].finish_reason == "tool_calls"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue