diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 3c5cbb65437..b9cbb52e57a 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -516,6 +516,29 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): return parameters +def _build_vertex_schema_for_gemini_2(parameters: dict) -> dict: + """ + Minimal schema builder for Gemini 2.0+ tool parameters. + + Gemini 2.0+ accepts standard JSON Schema natively in tool parameters, + including lowercase types, anyOf with null, and bare {} (TYPE_UNSPECIFIED). + The only transformation needed is resolving $ref/$defs, which Gemini does + NOT support in tool parameters (returns 400). + + This avoids the harmful transforms in _build_vertex_schema that break + JsonValue/Any semantics by coercing {} to {"type": "object"}. + """ + valid_schema_fields = set(get_type_hints(Schema).keys()) + + parameters = dict(parameters) # shallow copy to avoid mutating caller's dict + defs = parameters.pop("$defs", {}) + unpack_defs(parameters, defs) + + parameters = filter_schema_fields(parameters, valid_schema_fields) + + return parameters + + def _build_json_schema(parameters: dict) -> dict: """ Build a JSON Schema for use with Gemini's responseJsonSchema parameter. diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index e28b755be75..9b3a94904a7 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -97,6 +97,7 @@ from ..common_utils import ( VertexAIError, _build_json_schema, _build_vertex_schema, + _build_vertex_schema_for_gemini_2, supports_response_json_schema, ) from ..vertex_llm_base import VertexBase @@ -467,7 +468,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return None def _map_function( # noqa: PLR0915 - self, value: List[dict], optional_params: dict + self, value: List[dict], optional_params: dict, model: str = "" ) -> List[Tools]: """ Map OpenAI-style tools/functions to Vertex AI format. @@ -510,10 +511,21 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "parameters" in _openai_function_object and _openai_function_object["parameters"] is not None and isinstance(_openai_function_object["parameters"], dict) - ): # OPENAI accepts JSON Schema, Google accepts OpenAPI schema. - _openai_function_object["parameters"] = _build_vertex_schema( - _openai_function_object["parameters"] - ) + ): + if supports_response_json_schema(model): + # Gemini 2.0+: minimal transform (resolve $ref only) + _openai_function_object["parameters"] = ( + _build_vertex_schema_for_gemini_2( + _openai_function_object["parameters"] + ) + ) + else: + # Gemini 1.5: full OpenAPI-style transform + _openai_function_object["parameters"] = ( + _build_vertex_schema( + _openai_function_object["parameters"] + ) + ) openai_function_object = _openai_function_object @@ -1051,7 +1063,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ): # Pass optional_params so _map_function can add toolConfig if needed mapped_tools = self._map_function( - value=value, optional_params=optional_params + value=value, optional_params=optional_params, model=model ) optional_params = self._add_tools_to_optional_params( optional_params, mapped_tools diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index 94323e06901..a39c7da2c71 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -11,6 +11,7 @@ sys.path.insert( ) # Adds the parent directory to the system path from litellm.llms.vertex_ai.common_utils import ( + _build_vertex_schema_for_gemini_2, _get_vertex_url, convert_anyof_null_to_nullable, get_vertex_location_from_url, @@ -1402,3 +1403,93 @@ def test_add_object_type_does_not_add_type_when_anyof_present(): # Verify type was not added (anyOf handles the type) assert "type" not in input_schema, "type should not be added when anyOf is present" + + +class TestBuildVertexSchemaForGemini2: + """Tests for _build_vertex_schema_for_gemini_2 — minimal transform for Gemini 2.0+ tools.""" + + def test_jsonvalue_standalone_preserved(self): + """JsonValue (bare {}) should NOT be coerced to {"type": "object"}.""" + schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": {}, + }, + "required": ["name", "value"], + } + result = _build_vertex_schema_for_gemini_2(schema) + assert result["properties"]["value"] == {} + + def test_optional_jsonvalue_anyof_preserved(self): + """Optional[JsonValue] anyOf with null should be preserved, not converted to nullable.""" + schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": { + "anyOf": [ + {"type": "array", "items": {}}, + {}, + {"type": "null"}, + ] + }, + }, + "required": ["name"], + } + result = _build_vertex_schema_for_gemini_2(schema) + value_schema = result["properties"]["value"] + assert "anyOf" in value_schema + assert len(value_schema["anyOf"]) == 3 + assert {"type": "null"} in value_schema["anyOf"] + assert {} in value_schema["anyOf"] + + def test_ref_defs_resolved(self): + """$ref/$defs should be resolved since Gemini doesn't support them in tool params.""" + schema = { + "type": "object", + "properties": { + "value": {"$ref": "#/$defs/JsonValue"}, + }, + "$defs": {"JsonValue": {}}, + } + result = _build_vertex_schema_for_gemini_2(schema) + assert "$ref" not in result["properties"]["value"] + assert "$defs" not in result + assert result["properties"]["value"] == {} + + def test_unsupported_fields_stripped(self): + """Fields not in Vertex Schema TypedDict should be removed.""" + schema = { + "type": "object", + "properties": { + "name": {"type": "string", "additionalProperties": False}, + }, + "additionalProperties": False, + "$schema": "http://json-schema.org/draft-07/schema#", + } + result = _build_vertex_schema_for_gemini_2(schema) + assert "additionalProperties" not in result + assert "$schema" not in result + + def test_no_type_coercion(self): + """Schemas without type should NOT have type: object added.""" + schema = { + "type": "object", + "properties": { + "data": {"description": "Any data"}, + }, + } + result = _build_vertex_schema_for_gemini_2(schema) + assert "type" not in result["properties"]["data"] + + def test_items_empty_preserved(self): + """items: {} should NOT be coerced to items: {"type": "object"}.""" + schema = { + "type": "object", + "properties": { + "values": {"type": "array", "items": {}}, + }, + } + result = _build_vertex_schema_for_gemini_2(schema) + assert result["properties"]["values"]["items"] == {}