fix(gemini): preserve function calls during Gemini content policy checks

- Skip finishReason content_filter when candidate has functionCall parts
- Fix false positive from empty promptFeedback.blockReason
- Remove redundant thought_signatures from functionCall parts in
  message-level provider_specific_fields
This commit is contained in:
waani 2026-05-26 18:10:29 +08:00 • committed by chenbk7
parent c23b19f09c
commit 6583c35dff

View file

@ -1544,13 +1544,18 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
Per Google's docs, thoughtSignature is returned for multi-turn context preservation
and can appear on parts even without thought: true (e.g., regular text responses,
function calls). This method extracts all thoughtSignature values from parts.
function calls).
Only extracts signatures from non-functionCall parts — functionCall
thoughtSignatures are handled via tool_call.provider_specific_fields.
Returns:
List of thoughtSignature strings if any are found, None otherwise
"""
signatures: List[str] = []
for part in parts:
if "functionCall" in part:
continue
signature = part.get("thoughtSignature")
if signature is not None:
signatures.append(signature)
@ -2116,7 +2121,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
# Check if prompt is blocked due to content filtering
prompt_feedback = processed_chunk.get("promptFeedback")
if prompt_feedback and "blockReason" in prompt_feedback:
if prompt_feedback and prompt_feedback.get("blockReason"):
verbose_logger.debug(
f"Prompt blocked due to: {prompt_feedback.get('blockReason')} - {prompt_feedback.get('blockReasonMessage')}"
)
@ -2545,10 +2550,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
model_response.model = model
## CHECK IF RESPONSE FLAGGED
if (
"promptFeedback" in completion_response
and "blockReason" in completion_response["promptFeedback"]
):
if "promptFeedback" in completion_response and completion_response[
"promptFeedback"
].get("blockReason"):
return self._handle_blocked_response(
model_response=model_response,
completion_response=completion_response,
@ -2556,17 +2560,24 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
_candidates = completion_response.get("candidates")
if _candidates and len(_candidates) > 0:
content_policy_violations = (
VertexGeminiConfig().get_flagged_finish_reasons()
)
if (
"finishReason" in _candidates[0]
and _candidates[0]["finishReason"] in content_policy_violations.keys()
):
return self._handle_content_policy_violation(
model_response=model_response,
completion_response=completion_response,
# Don't short-circuit to content_filter if the candidate has
# functionCall parts — Gemini may return a safety finishReason
# alongside valid function call data.
candidate_parts = _candidates[0].get("content", {}).get("parts", [])
has_function_call = any("functionCall" in part for part in candidate_parts)
if not has_function_call:
content_policy_violations = (
VertexGeminiConfig().get_flagged_finish_reasons()
)
if (
"finishReason" in _candidates[0]
and _candidates[0]["finishReason"]
in content_policy_violations.keys()
):
return self._handle_content_policy_violation(
model_response=model_response,
completion_response=completion_response,
)
model_response.choices = []
response_id = completion_response.get("responseId")