fix(vertex_ai): normalize Gemini finish_reason enum through map_finis… (#25337)
* fix(vertex_ai): normalize Gemini finish_reason enum through map_finish_reason in streaming handler In the legacy vertex_ai SDK streaming path, the raw Gemini finish_reason enum name (e.g. "STOP", "MAX_TOKENS") was stored directly into self.received_finish_reason without being mapped to OpenAI-compatible values. The finish_reason_handler then compared against lowercase "stop", causing the case mismatch to prevent the tool_call override from ever firing. This fix applies map_finish_reason() so all Gemini enum names are normalized before storage.Refactor finish reason handling to use map_finish_reason function. * refactor: use module-level map_finish_reason import; drop redundant inline import map_finish_reason is already imported at module scope (line 49) via `from .core_helpers import map_finish_reason, process_response_headers`. The inline import added in the previous commit was redundant. Addressed Greptile review feedback.Removed unnecessary import of map_finish_reason from core_helpers. * test: add unit tests for Gemini legacy vertex finish_reason normalisation Added tests to ensure finish_reason normalization for Gemini legacy vertex tool calls and stop reasons.
This commit is contained in:
parent
8d945c86b7
commit
e6746270af
@ -1282,9 +1282,9 @@ class CustomStreamWrapper:
|
||||
and chunk.candidates[0].finish_reason.name # type: ignore
|
||||
!= "FINISH_REASON_UNSPECIFIED"
|
||||
): # every non-final chunk in vertex ai has this
|
||||
self.received_finish_reason = chunk.candidates[ # type: ignore
|
||||
0
|
||||
].finish_reason.name
|
||||
self.received_finish_reason = map_finish_reason( # type: ignore
|
||||
chunk.candidates[0].finish_reason.name
|
||||
)
|
||||
except Exception:
|
||||
if chunk.candidates[0].finish_reason.name == "SAFETY": # type: ignore
|
||||
raise Exception(
|
||||
|
||||
@ -1826,3 +1826,73 @@ async def test_custom_stream_wrapper_anext_exhaustion_raises_stop_async_iteratio
|
||||
pass # expected clean termination
|
||||
except RuntimeError as e:
|
||||
pytest.fail(f"PEP 479 regression: StopIteration leaked as RuntimeError: {e}")
|
||||
|
||||
|
||||
def test_gemini_legacy_vertex_stop_finish_reason_normalised():
|
||||
"""
|
||||
The legacy vertex_ai SDK streaming path sets finish_reason from a proto enum
|
||||
whose .name attribute is an uppercase string (e.g. "STOP", "MAX_TOKENS").
|
||||
Before the fix, received_finish_reason was stored as "STOP" which never
|
||||
matched "stop" in finish_reason_handler, silently breaking the tool_calls
|
||||
override. After the fix, map_finish_reason() is applied so the value is
|
||||
always an OpenAI-normalised lowercase string.
|
||||
"""
|
||||
wrapper = CustomStreamWrapper(
|
||||
completion_stream=None,
|
||||
model="gemini-1.5-pro",
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="vertex_ai",
|
||||
)
|
||||
|
||||
# Simulate a proto-like chunk: .candidates[0].finish_reason.name == "STOP"
|
||||
mock_finish_reason = MagicMock()
|
||||
mock_finish_reason.name = "STOP"
|
||||
mock_candidate = MagicMock()
|
||||
mock_candidate.finish_reason = mock_finish_reason
|
||||
mock_chunk = MagicMock()
|
||||
mock_chunk.candidates = [mock_candidate]
|
||||
# Ensure the chunk is not treated as a ModelResponseStream
|
||||
mock_chunk.__class__ = type("FakeProtoChunk", (), {})
|
||||
|
||||
with patch("litellm.litellm_core_utils.streaming_handler.proto", create=True):
|
||||
wrapper.chunk_creator(chunk=mock_chunk)
|
||||
|
||||
assert wrapper.received_finish_reason == "stop", (
|
||||
f"Expected 'stop' but got {wrapper.received_finish_reason!r}. "
|
||||
"map_finish_reason() was not applied to the Gemini enum name."
|
||||
)
|
||||
|
||||
|
||||
def test_gemini_legacy_vertex_tool_calls_finish_reason_with_stop_enum():
|
||||
"""
|
||||
When Gemini emits finish_reason STOP alongside tool-call content, the final
|
||||
chunk must report finish_reason='tool_calls'. This requires that the raw
|
||||
"STOP" enum name is first normalised to lowercase "stop" by map_finish_reason()
|
||||
so that finish_reason_handler's equality check fires correctly.
|
||||
"""
|
||||
wrapper = CustomStreamWrapper(
|
||||
completion_stream=None,
|
||||
model="gemini-1.5-pro",
|
||||
logging_obj=MagicMock(),
|
||||
custom_llm_provider="vertex_ai",
|
||||
)
|
||||
|
||||
mock_finish_reason = MagicMock()
|
||||
mock_finish_reason.name = "STOP"
|
||||
mock_candidate = MagicMock()
|
||||
mock_candidate.finish_reason = mock_finish_reason
|
||||
mock_chunk = MagicMock()
|
||||
mock_chunk.candidates = [mock_candidate]
|
||||
mock_chunk.__class__ = type("FakeProtoChunk", (), {})
|
||||
|
||||
with patch("litellm.litellm_core_utils.streaming_handler.proto", create=True):
|
||||
wrapper.chunk_creator(chunk=mock_chunk)
|
||||
|
||||
# Signal that tool_calls were present in the stream
|
||||
wrapper.tool_call = True
|
||||
|
||||
final = wrapper.finish_reason_handler()
|
||||
assert final.choices[0].finish_reason == "tool_calls", (
|
||||
f"Expected 'tool_calls' but got {final.choices[0].finish_reason!r}. "
|
||||
"STOP enum was not normalised through map_finish_reason()."
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user