fix(gemini): dereference $defs/$ref in tool response content (#19062)

Gemini API rejects JSON schemas with $defs/$ref references anywhere in
the conversation, including in function_response content. This causes
errors when tools return JSON containing schemas (e.g., toolbelt_inspect_tool
returning tool definitions).

The fix:
1. Apply unpack_defs() to all JSON tool responses before sending to Gemini
2. Recursively remove $defs sections after inlining references
3. Replace any remaining $ref (circular refs, external URLs) with placeholders

Edge cases handled:
- Circular $ref (self-referential types like TreeNode.left -> TreeNode)
- External $ref (URL-based like https://...)
- Deeply nested $ref in anyOf/oneOf/allOf

This ensures function_response content is clean before being sent to the
Gemini API.

Related issues:
- https://github.com/google-gemini/gemini-cli/issues/13326
- FastMCP #1372, #1426
This commit is contained in:
Debnil Sur 2026-01-14 08:58:28 -08:00 committed by GitHub
parent cd2d381dd4
commit 84dad952a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -45,6 +45,7 @@ from .common_utils import (
infer_content_type_from_url_and_content,
is_non_content_values_set,
parse_tool_call_arguments,
unpack_defs,
)
from .image_handling import convert_url_to_base64
@ -1462,6 +1463,56 @@ def convert_to_gemini_tool_call_invoke(
)
def _clean_refs_for_gemini(obj: Any) -> None:
"""
Recursively clean $defs, $ref, and definitions from a dict for Gemini compatibility.
Gemini rejects:
- $defs sections (even after $ref has been inlined)
- Any remaining $ref (circular refs, external URLs)
This function:
1. Removes all $defs/definitions keys
2. Replaces any remaining $ref with a placeholder object
"""
if isinstance(obj, dict):
# Remove $defs and definitions at this level
obj.pop("$defs", None)
obj.pop("definitions", None)
# Check for and handle remaining $ref (circular or external)
if "$ref" in obj:
ref_value = obj.pop("$ref")
# Replace with a generic object type as placeholder
obj["type"] = "object"
obj["description"] = f"(schema reference: {ref_value})"
# Recurse into values
for value in obj.values():
_clean_refs_for_gemini(value)
elif isinstance(obj, list):
for item in obj:
_clean_refs_for_gemini(item)
def _prepare_response_for_gemini(response_data: dict) -> dict:
"""
Prepare a tool response dict for Gemini by inlining $ref and removing $defs.
Gemini rejects JSON schemas with $defs/$ref in function_response content.
This function applies unpack_defs to inline references, then cleans up
any remaining $defs sections and unresolved $refs (circular or external).
Returns a new dict (does not mutate the input).
"""
import copy
result = copy.deepcopy(response_data)
unpack_defs(result, {})
_clean_refs_for_gemini(result)
return result
def convert_to_gemini_tool_call_result(
message: Union[ChatCompletionToolMessage, ChatCompletionFunctionMessage],
last_message_with_tool_calls: Optional[dict],
@ -1570,6 +1621,11 @@ def convert_to_gemini_tool_call_result(
# Not valid JSON, wrap in content field
response_data = {"content": content_str}
# Gemini rejects JSON schemas with $defs/$ref in function_response content.
# Inline $refs and clean up for Gemini compatibility.
if isinstance(response_data, dict):
response_data = _prepare_response_for_gemini(response_data)
# We can't determine from openai message format whether it's a successful or
# error call result so default to the successful result template
_function_response = VertexFunctionResponse(