mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix: Handle Gemini empty responses with finishReason=STOP but no content/parts
When Gemini (particularly gemini-2.5-flash-lite) returns candidates with finishReason=STOP but without the 'parts' field in content (e.g., in long-running agentic tasks), _process_candidates skips these candidates because it requires 'parts' to be present. This resulted in an empty choices list without any error being raised. This fix adds a fallback after _process_candidates: if choices is still empty but candidates exist, create a choice with finish_reason=stop and content=None. This is consistent with the streaming path (lines 3080-3097) which already handles this case. Fixes https://github.com/BerriAI/litellm/issues/24442
This commit is contained in:
parent
3292d02aa4
commit
a8594dac23
2 changed files with 84 additions and 0 deletions
|
|
@ -2375,6 +2375,25 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
_candidates, model_response, logging_obj.optional_params
|
||||
)
|
||||
|
||||
# Handle the case where _process_candidates produced no choices.
|
||||
# This can happen when Gemini returns candidates with finishReason=STOP
|
||||
# but no content/parts (e.g., empty response in agentic tasks with
|
||||
# gemini-2.5-flash-lite). See https://discuss.ai.google.dev/t/
|
||||
# finishreason-stop-but-parts-is-missing-inside-candidate/99331
|
||||
if not model_response.choices and _candidates:
|
||||
for candidate in _candidates:
|
||||
finish_reason_str = candidate.get("finishReason")
|
||||
choice = litellm.Choices(
|
||||
finish_reason=VertexGeminiConfig._check_finish_reason(
|
||||
None, finish_reason_str
|
||||
),
|
||||
index=candidate.get("index", 0),
|
||||
message={"role": "assistant", "content": None},
|
||||
logprobs=None,
|
||||
enhancements=None,
|
||||
)
|
||||
model_response.choices.append(choice)
|
||||
|
||||
usage = VertexGeminiConfig._calculate_usage(
|
||||
completion_response=completion_response
|
||||
)
|
||||
|
|
|
|||
|
|
@ -672,6 +672,71 @@ def test_check_finish_reason():
|
|||
)
|
||||
|
||||
|
||||
def test_vertex_ai_empty_content_with_stop_finish_reason():
|
||||
"""
|
||||
Test that Gemini responses with finishReason=STOP but no content/parts
|
||||
(empty response in agentic tasks) are handled correctly.
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/24442
|
||||
See: https://discuss.ai.google.dev/t/finishreason-stop-but-parts-is-missing-inside-candidate/99331
|
||||
"""
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
from litellm.types.llms.vertex_ai import GenerateContentResponseBody
|
||||
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
# Simulate the raw API response that has finishReason=STOP but no parts
|
||||
raw_response = {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"role": "model"
|
||||
# Note: no "parts" key - this is the bug condition
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
"usageMetadata": {
|
||||
"promptTokenCount": 3130,
|
||||
"totalTokenCount": 3130,
|
||||
"promptTokensDetails": [
|
||||
{"modality": "TEXT", "tokenCount": 3130}
|
||||
],
|
||||
},
|
||||
"modelVersion": "gemini-2.5-flash-lite",
|
||||
"responseId": "8ZfBaYu9LarVz7IPlJG7gQU",
|
||||
}
|
||||
|
||||
# Mock the logging object
|
||||
mock_logging_obj = MagicMock()
|
||||
mock_logging_obj.optional_params = {}
|
||||
|
||||
# Transform the response
|
||||
model_response = ModelResponse()
|
||||
model_response._hidden_params = {}
|
||||
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
|
||||
result = v._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=GenerateContentResponseBody(**raw_response),
|
||||
model_response=model_response,
|
||||
model="gemini-2.5-flash-lite",
|
||||
logging_obj=mock_logging_obj,
|
||||
raw_response=MagicMock(headers={}),
|
||||
)
|
||||
|
||||
# The response should have a choice with finish_reason=stop and content=None
|
||||
assert len(result.choices) == 1
|
||||
assert result.choices[0].finish_reason == "stop"
|
||||
assert result.choices[0].message.content is None
|
||||
assert result.choices[0].message.role == "assistant"
|
||||
|
||||
|
||||
def test_finish_reason_unspecified_and_malformed_function_call():
|
||||
"""
|
||||
Test that FINISH_REASON_UNSPECIFIED and MALFORMED_FUNCTION_CALL
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue