mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(gemini): preserve $ref in JSON Schema for Gemini 2.0+ to avoid nesting depth errors
This commit is contained in:
parent
2c8fcf854a
commit
3fe331ed7d
4 changed files with 130 additions and 31 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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]):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue