|
38 | 38 | if TYPE_CHECKING: |
39 | 39 | from google.adk.models.llm_request import LlmRequest |
40 | 40 |
|
| 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 | + |
41 | 44 |
|
42 | 45 | def _convert_role_to_openai(role: Optional[str]) -> str: |
43 | 46 | """Convert google.genai role to OpenAI role.""" |
@@ -316,12 +319,22 @@ def _convert_tools_to_openai(tools: list[types.Tool]) -> list[ChatCompletionTool |
316 | 319 |
|
317 | 320 | def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmResponse: |
318 | 321 | """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 | + |
319 | 331 | if not response.choices: |
320 | 332 | return LlmResponse( |
321 | 333 | content=types.Content( |
322 | 334 | role="model", |
323 | | - parts=[types.Part.from_text(text="Response blocked by content policy.")], |
| 335 | + parts=[types.Part.from_text(text=_CONTENT_BLOCKED_PLACEHOLDER)], |
324 | 336 | ), |
| 337 | + usage_metadata=usage_metadata, |
325 | 338 | finish_reason=types.FinishReason.SAFETY, |
326 | 339 | ) |
327 | 340 | choice = response.choices[0] |
@@ -354,15 +367,6 @@ def _convert_openai_response_to_llm_response(response: ChatCompletion) -> LlmRes |
354 | 367 |
|
355 | 368 | content = types.Content(role="model", parts=parts) |
356 | 369 |
|
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 | | - |
366 | 370 | # Handle finish reason |
367 | 371 | finish_reason = types.FinishReason.STOP |
368 | 372 | if choice.finish_reason == "length": |
@@ -593,8 +597,12 @@ async def generate_content_async( |
593 | 597 | # Guardrail or content filter can produce zero content/tool chunks. |
594 | 598 | # An empty parts list causes downstream IndexError; emit a placeholder. |
595 | 599 | 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 |
598 | 606 |
|
599 | 607 | # Always yield final response to signal completion and valid metadata |
600 | 608 | final_content = types.Content(role="model", parts=final_parts) |
|
0 commit comments