-
Notifications
You must be signed in to change notification settings - Fork 617
fix(adk): handle empty choices and zero-content streaming chunks from guardrail-blocked responses #1993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(adk): handle empty choices and zero-content streaming chunks from guardrail-blocked responses #1993
Changes from all commits
fbf0a1c
834673e
77a0fa0
9f4e599
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,9 @@ | |
| if TYPE_CHECKING: | ||
| from google.adk.models.llm_request import LlmRequest | ||
|
|
||
| # Emitted when a guardrail or content filter blocks a response, leaving no content to surface. | ||
| _CONTENT_BLOCKED_PLACEHOLDER = "Response blocked by content policy." | ||
|
|
||
|
|
||
| def _convert_role_to_openai(role: Optional[str]) -> str: | ||
| """Convert google.genai role to OpenAI role.""" | ||
|
|
@@ -316,6 +319,24 @@ def _convert_tools_to_openai(tools: list[types.Tool]) -> list[ChatCompletionTool | |
|
|
||
| def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmResponse: | ||
| """Convert OpenAI response to LlmResponse.""" | ||
| # Handle usage metadata | ||
| usage_metadata = None | ||
| if hasattr(response, "usage") and response.usage: | ||
| usage_metadata = types.GenerateContentResponseUsageMetadata( | ||
| prompt_token_count=response.usage.prompt_tokens, | ||
| candidates_token_count=response.usage.completion_tokens, | ||
| total_token_count=response.usage.total_tokens, | ||
| ) | ||
|
|
||
| if not response.choices: | ||
| return LlmResponse( | ||
| content=types.Content( | ||
| role="model", | ||
| parts=[types.Part.from_text(text=_CONTENT_BLOCKED_PLACEHOLDER)], | ||
| ), | ||
| usage_metadata=usage_metadata, | ||
| finish_reason=types.FinishReason.SAFETY, | ||
| ) | ||
| choice = response.choices[0] | ||
| message = choice.message | ||
|
|
||
|
|
@@ -346,15 +367,6 @@ def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmRes | |
|
|
||
| content = types.Content(role="model", parts=parts) | ||
|
|
||
| # Handle usage metadata | ||
| usage_metadata = None | ||
| if hasattr(response, "usage") and response.usage: | ||
| usage_metadata = types.GenerateContentResponseUsageMetadata( | ||
| prompt_token_count=response.usage.prompt_tokens, | ||
| candidates_token_count=response.usage.completion_tokens, | ||
| total_token_count=response.usage.total_tokens, | ||
| ) | ||
|
|
||
| # Handle finish reason | ||
| finish_reason = types.FinishReason.STOP | ||
| if choice.finish_reason == "length": | ||
|
|
@@ -582,6 +594,16 @@ async def generate_content_async( | |
| elif finish_reason == "tool_calls": | ||
| final_reason = types.FinishReason.STOP # Tool calls is a normal completion | ||
|
|
||
| # Guardrail or content filter can produce zero content/tool chunks. | ||
| # An empty parts list causes downstream IndexError; emit a placeholder. | ||
| if not final_parts: | ||
| if final_reason == types.FinishReason.MAX_TOKENS: | ||
| # Truncated by length before any content; not a safety block. | ||
| final_parts.append(types.Part.from_text(text="")) | ||
| else: | ||
| final_parts.append(types.Part.from_text(text=_CONTENT_BLOCKED_PLACEHOLDER)) | ||
| final_reason = types.FinishReason.SAFETY | ||
|
Comment on lines
+597
to
+605
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping the broad fallback intentionally: the motivating case is AWS Bedrock guardrails (via agentgateway), which block responses without setting |
||
|
|
||
| # Always yield final response to signal completion and valid metadata | ||
| final_content = types.Content(role="model", parts=final_parts) | ||
| yield LlmResponse( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in f49c71d — usage extraction is now hoisted above the empty-choices guard and included in the early return.