diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 02b69b94d94..6938e499522 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -524,7 +524,7 @@ def _build_json_schema(parameters: dict) -> dict: - Does NOT convert types to uppercase (keeps standard JSON Schema format) - Does NOT add propertyOrdering - Does NOT filter fields (allows additionalProperties) - - Still unpacks $defs/$ref (Gemini doesn't support JSON Schema references) + - Preserves $defs/$ref (Gemini 2.0+ supports JSON Schema references natively) Parameters: parameters: dict - the JSON schema to process @@ -532,24 +532,12 @@ def _build_json_schema(parameters: dict) -> dict: Returns: dict - the processed schema in standard JSON Schema format """ - # Unpack $defs references (Gemini doesn't support $ref) - defs = parameters.pop("$defs", {}) - for name, value in defs.items(): - unpack_defs(value, defs) - unpack_defs(parameters, defs) - - # Convert anyOf with null to nullable - convert_anyof_null_to_nullable(parameters) - - # Handle empty strings in enum values - Gemini doesn't accept empty strings in enums - _fix_enum_empty_strings(parameters) - - # Remove enums for non-string typed fields (Gemini requires enum only on strings) - _fix_enum_types(parameters) - - # Handle empty items objects - process_items(parameters) - add_object_type(parameters) + # Gemini 2.0+ with responseJsonSchema accepts standard JSON Schema as-is, + # including $ref, $defs, anyOf, etc. No transformations needed — the + # OpenAPI-specific fixes (unpack_defs, add_object_type, convert_anyof, etc.) + # are only required for responseSchema (Gemini 1.5) and can break valid + # JSON Schema by adding conflicting fields to $ref nodes. + # See: https://blog.google/technology/developers/gemini-api-structured-outputs/ return parameters 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 cf3461a9960..8558ca61b98 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 @@ -14,6 +14,7 @@ from typing import ( Literal, Optional, Tuple, + Type, Union, cast, ) @@ -106,6 +107,8 @@ from .transformation import ( ) if TYPE_CHECKING: + from pydantic import BaseModel + from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.types.utils import ModelResponseStream, StreamingChoices @@ -226,6 +229,47 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): def get_config(cls): return super().get_config() + def get_json_schema_from_pydantic_object( + self, response_format: Optional[Union[Type["BaseModel"], dict]] + ) -> Optional[dict]: + """ + Override to use Pydantic's model_json_schema() instead of OpenAI's + to_strict_json_schema(). + + OpenAI's to_strict_json_schema() inlines all $ref references, which + dramatically increases schema nesting depth and causes Gemini to reject + schemas with 'exceeds maximum allowed nesting depth' errors. + + Pydantic's model_json_schema() preserves $ref/$defs, keeping the schema + compact. Gemini 2.0+ (responseJsonSchema) natively supports $ref, and + Gemini 1.5 (responseSchema) handles unpacking via _build_vertex_schema. + + See: https://github.com/BerriAI/litellm/issues/21014 + """ + from pydantic import BaseModel as _BaseModel + + if response_format is None: + return None + + if isinstance(response_format, dict): + return response_format + + if isinstance(response_format, type) and issubclass( + response_format, _BaseModel + ): + schema = response_format.model_json_schema() + return { + "type": "json_schema", + "json_schema": { + "schema": schema, + "name": response_format.__name__, + "strict": True, + }, + } + + # Fallback: delegate to parent for unknown types + return super().get_json_schema_from_pydantic_object(response_format) + @staticmethod def _is_gemini_3_or_newer(model: str) -> bool: """ diff --git a/litellm/utils.py b/litellm/utils.py index 241b9d217b7..f29eb83c908 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -3865,18 +3865,6 @@ def get_optional_params( # noqa: PLR0915 ): passed_params = locals().copy() special_params = passed_params.pop("kwargs") - non_default_params = pre_process_non_default_params( - passed_params=passed_params, - special_params=special_params, - custom_llm_provider=custom_llm_provider, - additional_drop_params=additional_drop_params, - model=model, - ) - optional_params = pre_process_optional_params( - passed_params=passed_params, - non_default_params=non_default_params, - custom_llm_provider=custom_llm_provider, - ) provider_config: Optional[BaseConfig] = None if custom_llm_provider is not None and custom_llm_provider in [ provider.value for provider in LlmProviders @@ -3884,6 +3872,19 @@ def get_optional_params( # noqa: PLR0915 provider_config = ProviderConfigManager.get_provider_chat_config( model=model, provider=LlmProviders(custom_llm_provider) ) + non_default_params = pre_process_non_default_params( + passed_params=passed_params, + special_params=special_params, + custom_llm_provider=custom_llm_provider, + additional_drop_params=additional_drop_params, + model=model, + provider_config=provider_config, + ) + optional_params = pre_process_optional_params( + passed_params=passed_params, + non_default_params=non_default_params, + custom_llm_provider=custom_llm_provider, + ) def _check_valid_arg(supported_params: List[str]): """ diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index bef3a69bb90..d009b563adf 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -210,6 +210,72 @@ def test_vertex_ai_response_schema_defs(): } +def test_vertex_ai_response_json_schema_preserves_refs_for_gemini_2(): + """ + Test that $defs and $ref are preserved for Gemini 2.0+ models using responseJsonSchema. + + Gemini 2.0+ supports standard JSON Schema with $ref/$defs natively. + Unpacking them inflates nesting depth and can exceed Gemini's limit. + """ + v = VertexGeminiConfig() + + schema = cast(dict, v.get_json_schema_from_pydantic_object(MathReasoning)) + + # Pydantic generates $defs with $ref — verify our test input has them + assert "$defs" in schema["json_schema"]["schema"] + + transformed_request = v.map_openai_params( + non_default_params={ + "messages": [{"role": "user", "content": "Hello, world!"}], + "response_format": schema, + }, + optional_params={}, + model="gemini-2.5-flash", # Gemini 2.0+ uses responseJsonSchema + drop_params=False, + ) + + # $defs and $ref should be preserved (not unpacked) + assert "response_json_schema" in transformed_request + result_schema = transformed_request["response_json_schema"] + assert "$defs" in result_schema, "responseJsonSchema should preserve $defs for Gemini 2.0+" + + +def test_vertex_ai_get_json_schema_preserves_refs_for_nested_pydantic(): + """ + Test that get_json_schema_from_pydantic_object uses model_json_schema() + (which preserves $ref/$defs) instead of OpenAI's to_strict_json_schema() + (which inlines all $ref, inflating nesting depth). + + This is the root cause fix for https://github.com/BerriAI/litellm/issues/21014 + """ + from pydantic import Field + + class Inner(BaseModel): + value: str = Field(description="A value") + + class Outer(BaseModel): + first: Inner = Field(description="First inner") + second: Inner = Field(description="Second inner") + + # VertexGeminiConfig override should preserve $ref + config = VertexGeminiConfig() + result = config.get_json_schema_from_pydantic_object(Outer) + + assert result is not None + schema = result["json_schema"]["schema"] + schema_str = json.dumps(schema) + + # model_json_schema() produces $ref/$defs; to_strict_json_schema() inlines them + assert "$defs" in schema, "Schema should have $defs (not inlined)" + assert "$ref" in schema_str, "Schema should have $ref references (not inlined)" + + # GoogleAIStudioGeminiConfig inherits the same behavior + gemini_config = GoogleAIStudioGeminiConfig() + result2 = gemini_config.get_json_schema_from_pydantic_object(Outer) + schema2 = result2["json_schema"]["schema"] + assert "$defs" in schema2, "GoogleAIStudioGeminiConfig should also preserve $defs" + + def test_vertex_ai_response_json_schema_for_gemini_2(): """ Test that Gemini 2.0+ models automatically use responseJsonSchema.