Skip to content

Commit 834673e

Browse files
committed
fix(adk): address review feedback on guardrail fallback paths
- Preserve usage_metadata when choices is empty - Keep MAX_TOKENS finish reason when an empty stream was truncated by length rather than blocked by a content filter - Centralize the blocked-content placeholder in a module constant Signed-off-by: Combrink van der Vyver <combrink@gmail.com>
1 parent fbf0a1c commit 834673e

2 files changed

Lines changed: 39 additions & 12 deletions

File tree

python/packages/kagent-adk/src/kagent/adk/models/_openai.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
if TYPE_CHECKING:
3939
from google.adk.models.llm_request import LlmRequest
4040

41+
# Emitted when a guardrail or content filter blocks a response, leaving no content to surface.
42+
_CONTENT_BLOCKED_PLACEHOLDER = "Response blocked by content policy."
43+
4144

4245
def _convert_role_to_openai(role: Optional[str]) -> str:
4346
"""Convert google.genai role to OpenAI role."""
@@ -316,12 +319,22 @@ def _convert_tools_to_openai(tools: list[types.Tool]) -> list[ChatCompletionTool
316319

317320
def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmResponse:
318321
"""Convert OpenAI response to LlmResponse."""
322+
# Handle usage metadata
323+
usage_metadata = None
324+
if hasattr(response, "usage") and response.usage:
325+
usage_metadata = types.GenerateContentResponseUsageMetadata(
326+
prompt_token_count=response.usage.prompt_tokens,
327+
candidates_token_count=response.usage.completion_tokens,
328+
total_token_count=response.usage.total_tokens,
329+
)
330+
319331
if not response.choices:
320332
return LlmResponse(
321333
content=types.Content(
322334
role="model",
323-
parts=[types.Part.from_text(text="Response blocked by content policy.")],
335+
parts=[types.Part.from_text(text=_CONTENT_BLOCKED_PLACEHOLDER)],
324336
),
337+
usage_metadata=usage_metadata,
325338
finish_reason=types.FinishReason.SAFETY,
326339
)
327340
choice = response.choices[0]
@@ -354,15 +367,6 @@ def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmRes
354367

355368
content = types.Content(role="model", parts=parts)
356369

357-
# Handle usage metadata
358-
usage_metadata = None
359-
if hasattr(response, "usage") and response.usage:
360-
usage_metadata = types.GenerateContentResponseUsageMetadata(
361-
prompt_token_count=response.usage.prompt_tokens,
362-
candidates_token_count=response.usage.completion_tokens,
363-
total_token_count=response.usage.total_tokens,
364-
)
365-
366370
# Handle finish reason
367371
finish_reason = types.FinishReason.STOP
368372
if choice.finish_reason == "length":
@@ -593,8 +597,12 @@ async def generate_content_async(
593597
# Guardrail or content filter can produce zero content/tool chunks.
594598
# An empty parts list causes downstream IndexError; emit a placeholder.
595599
if not final_parts:
596-
final_parts.append(types.Part.from_text(text="Response blocked by content policy."))
597-
final_reason = types.FinishReason.SAFETY
600+
if final_reason == types.FinishReason.MAX_TOKENS:
601+
# Truncated by length before any content; not a safety block.
602+
final_parts.append(types.Part.from_text(text=""))
603+
else:
604+
final_parts.append(types.Part.from_text(text=_CONTENT_BLOCKED_PLACEHOLDER))
605+
final_reason = types.FinishReason.SAFETY
598606

599607
# Always yield final response to signal completion and valid metadata
600608
final_content = types.Content(role="model", parts=final_parts)

python/packages/kagent-adk/tests/unittests/models/test_openai.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,23 @@ async def gen():
567567
assert final_response.finish_reason == types.FinishReason.SAFETY
568568
assert final_response.content.parts[0].text == "Response blocked by content policy."
569569

570+
# An empty stream truncated by length keeps MAX_TOKENS instead of being marked SAFETY.
571+
with mock.patch.object(openai_llm, "_client") as mock_client:
572+
573+
async def mock_length_stream_gen_func(*args, **kwargs):
574+
async def gen():
575+
yield MockChunk(finish_reason="length")
576+
577+
return gen()
578+
579+
mock_client.chat.completions.create.side_effect = mock_length_stream_gen_func
580+
581+
stream_results = [resp async for resp in openai_llm.generate_content_async(llm_request, stream=True)]
582+
583+
final_response = stream_results[-1]
584+
assert final_response.finish_reason == types.FinishReason.MAX_TOKENS
585+
assert final_response.content.parts[0].text == ""
586+
570587

571588
# ============================================================================
572589
# SSL/TLS Configuration Tests
@@ -988,6 +1005,8 @@ def test_empty_choices_returns_safety_finish_reason(self):
9881005

9891006
assert llm_response.finish_reason == types.FinishReason.SAFETY
9901007
assert llm_response.content.parts[0].text == "Response blocked by content policy."
1008+
assert llm_response.usage_metadata is not None
1009+
assert llm_response.usage_metadata.total_token_count == 8
9911010

9921011
def test_preserves_thought_signature_from_openai_tool_call_response(self):
9931012
response = self._MockResponse(

0 commit comments

Comments
 (0)