fix: Don't set error_code and error_message when model stop response in normal case

PiperOrigin-RevId: 783415488
This commit is contained in:
Xiang (Sean) Zhou
2025-07-15 11:47:53 -07:00
committed by Copybara-Service
parent ce037ec888
commit 3c6e18fd1c
2 changed files with 175 additions and 5 deletions
+7 -2
View File
@@ -27,6 +27,7 @@ from typing import Union
from google.genai import Client
from google.genai import types
from google.genai.types import FinishReason
from typing_extensions import override
from .. import version
@@ -161,8 +162,12 @@ class Gemini(BaseLlm):
parts.append(types.Part.from_text(text=text))
yield LlmResponse(
content=types.ModelContent(parts=parts),
error_code=response.candidates[0].finish_reason,
error_message=response.candidates[0].finish_message,
error_code=None
if response.candidates[0].finish_reason == FinishReason.STOP
else response.candidates[0].finish_reason,
error_message=None
if response.candidates[0].finish_reason == FinishReason.STOP
else response.candidates[0].finish_message,
usage_metadata=usage_metadata,
)
+168 -3
View File
@@ -28,6 +28,7 @@ from google.adk.utils.variant_utils import GoogleLLMVariant
from google.genai import types
from google.genai import version as genai_version
from google.genai.types import Content
from google.genai.types import FinishReason
from google.genai.types import Part
import pytest
@@ -754,6 +755,7 @@ async def test_generate_content_async_stream_aggregated_content_regardless_of_fi
# Final response should have aggregated content with error info
final_response = responses[2]
assert final_response.content.parts[0].text == "Hello world"
# After the code changes, error_code and error_message are set for non-STOP finish reasons
assert final_response.error_code == finish_reason
assert final_response.error_message == f"Finished with {finish_reason}"
@@ -835,6 +837,163 @@ async def test_generate_content_async_stream_with_thought_and_text_error_handlin
assert final_response.content.parts[0].text == "Think1"
assert final_response.content.parts[0].thought is True
assert final_response.content.parts[1].text == "Answer"
# After the code changes, error_code and error_message are set for non-STOP finish reasons
assert final_response.error_code == types.FinishReason.MAX_TOKENS
assert final_response.error_message == "Maximum tokens reached"
@pytest.mark.asyncio
async def test_generate_content_async_stream_error_info_none_for_stop_finish_reason():
"""Test that error_code and error_message are None when finish_reason is STOP."""
gemini_llm = Gemini(model="gemini-1.5-flash")
llm_request = LlmRequest(
model="gemini-1.5-flash",
contents=[Content(role="user", parts=[Part.from_text(text="Hello")])],
config=types.GenerateContentConfig(
temperature=0.1,
response_modalities=[types.Modality.TEXT],
system_instruction="You are a helpful assistant",
),
)
with mock.patch.object(gemini_llm, "api_client") as mock_client:
class MockAsyncIterator:
def __init__(self, seq):
self.iter = iter(seq)
def __aiter__(self):
return self
async def __anext__(self):
try:
return next(self.iter)
except StopIteration:
raise StopAsyncIteration
mock_responses = [
types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model", parts=[Part.from_text(text="Hello")]
),
finish_reason=None,
)
]
),
types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model", parts=[Part.from_text(text=" world")]
),
finish_reason=types.FinishReason.STOP,
finish_message="Successfully completed",
)
]
),
]
async def mock_coro():
return MockAsyncIterator(mock_responses)
mock_client.aio.models.generate_content_stream.return_value = mock_coro()
responses = [
resp
async for resp in gemini_llm.generate_content_async(
llm_request, stream=True
)
]
# Should have 3 responses: 2 partial and 1 final aggregated
assert len(responses) == 3
assert responses[0].partial is True
assert responses[1].partial is True
# Final response should have aggregated content with error info None for STOP finish reason
final_response = responses[2]
assert final_response.content.parts[0].text == "Hello world"
assert final_response.error_code is None
assert final_response.error_message is None
@pytest.mark.asyncio
async def test_generate_content_async_stream_error_info_set_for_non_stop_finish_reason():
"""Test that error_code and error_message are set for non-STOP finish reasons."""
gemini_llm = Gemini(model="gemini-1.5-flash")
llm_request = LlmRequest(
model="gemini-1.5-flash",
contents=[Content(role="user", parts=[Part.from_text(text="Hello")])],
config=types.GenerateContentConfig(
temperature=0.1,
response_modalities=[types.Modality.TEXT],
system_instruction="You are a helpful assistant",
),
)
with mock.patch.object(gemini_llm, "api_client") as mock_client:
class MockAsyncIterator:
def __init__(self, seq):
self.iter = iter(seq)
def __aiter__(self):
return self
async def __anext__(self):
try:
return next(self.iter)
except StopIteration:
raise StopAsyncIteration
mock_responses = [
types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model", parts=[Part.from_text(text="Hello")]
),
finish_reason=None,
)
]
),
types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model", parts=[Part.from_text(text=" world")]
),
finish_reason=types.FinishReason.MAX_TOKENS,
finish_message="Maximum tokens reached",
)
]
),
]
async def mock_coro():
return MockAsyncIterator(mock_responses)
mock_client.aio.models.generate_content_stream.return_value = mock_coro()
responses = [
resp
async for resp in gemini_llm.generate_content_async(
llm_request, stream=True
)
]
# Should have 3 responses: 2 partial and 1 final aggregated
assert len(responses) == 3
assert responses[0].partial is True
assert responses[1].partial is True
# Final response should have aggregated content with error info set for non-STOP finish reason
final_response = responses[2]
assert final_response.content.parts[0].text == "Hello world"
assert final_response.error_code == types.FinishReason.MAX_TOKENS
assert final_response.error_message == "Maximum tokens reached"
@@ -1023,7 +1182,9 @@ async def test_generate_content_async_stream_mixed_text_function_call_text():
# Final aggregated text with error info
assert responses[4].content.parts[0].text == " second text"
assert responses[4].error_code == types.FinishReason.STOP
assert (
responses[4].error_code is None
) # STOP finish reason should have None error_code
@pytest.mark.asyncio
@@ -1220,7 +1381,9 @@ async def test_generate_content_async_stream_complex_mixed_thought_text_function
# Final aggregated response should have both thought and text
final_response = responses[-1]
assert final_response.error_code == types.FinishReason.STOP
assert (
final_response.error_code is None
) # STOP finish reason should have None error_code
assert len(final_response.content.parts) == 2 # thought part + text part
assert final_response.content.parts[0].thought is True
assert "More thinking..." in final_response.content.parts[0].text
@@ -1356,7 +1519,9 @@ async def test_generate_content_async_stream_two_separate_text_aggregations():
# Final aggregation should contain "Second chunk" and have error info
final_aggregation = aggregated_text_responses[-1]
assert final_aggregation.content.parts[0].text == "Second chunk"
assert final_aggregation.error_code == types.FinishReason.STOP
assert (
final_aggregation.error_code is None
) # STOP finish reason should have None error_code
# Verify the function call is preserved between aggregations
function_call_responses = [