mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-22 16:24:14 +00:00
Merge pull request #19080 from BerriAI/revert-18147-feat/gemini-response-json-schema
Revert "feat(gemini): add opt-in support for responseJsonSchema"
This commit is contained in:
@@ -341,90 +341,4 @@ curl http://0.0.0.0:4000/v1/chat/completions \
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Gemini - Native JSON Schema Format (Gemini 2.0+)
|
||||
|
||||
Gemini 2.0+ models automatically use the native `responseJsonSchema` parameter, which provides better compatibility with standard JSON Schema format.
|
||||
|
||||
### Benefits (Gemini 2.0+):
|
||||
- Standard JSON Schema format (lowercase types like `string`, `object`)
|
||||
- Supports `additionalProperties: false` for stricter validation
|
||||
- Better compatibility with Pydantic's `model_json_schema()`
|
||||
- No `propertyOrdering` required
|
||||
|
||||
### Usage
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
```python
|
||||
from litellm import completion
|
||||
from pydantic import BaseModel
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
response = completion(
|
||||
model="gemini/gemini-2.0-flash",
|
||||
messages=[{"role": "user", "content": "Extract: John is 25 years old"}],
|
||||
response_format={
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "user_info",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
},
|
||||
"required": ["name", "age"],
|
||||
"additionalProperties": False # Supported on Gemini 2.0+
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
```bash
|
||||
curl http://0.0.0.0:4000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $LITELLM_API_KEY" \
|
||||
-d '{
|
||||
"model": "gemini-2.0-flash",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Extract: John is 25 years old"}
|
||||
],
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "user_info",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
},
|
||||
"required": ["name", "age"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Model Behavior
|
||||
|
||||
| Model | Format Used | `additionalProperties` Support |
|
||||
|-------|-------------|-------------------------------|
|
||||
| Gemini 2.0+ | `responseJsonSchema` (JSON Schema) | ✅ Yes |
|
||||
| Gemini 1.5 | `responseSchema` (OpenAPI) | ❌ No |
|
||||
|
||||
LiteLLM automatically selects the appropriate format based on the model version.
|
||||
</Tabs>
|
||||
@@ -150,34 +150,6 @@ def get_supports_response_schema(
|
||||
return _supports_response_schema
|
||||
|
||||
|
||||
def supports_response_json_schema(model: str) -> bool:
|
||||
"""
|
||||
Check if the model supports responseJsonSchema (JSON Schema format).
|
||||
|
||||
responseJsonSchema is supported by Gemini 2.0+ models and uses standard
|
||||
JSON Schema format with lowercase types (string, object, etc.) instead of
|
||||
the OpenAPI-style responseSchema with uppercase types (STRING, OBJECT, etc.).
|
||||
|
||||
Benefits of responseJsonSchema:
|
||||
- Supports additionalProperties for stricter schema validation
|
||||
- Uses standard JSON Schema format (no type conversion needed)
|
||||
- Better compatibility with Pydantic's model_json_schema()
|
||||
|
||||
Args:
|
||||
model: The model name (e.g., "gemini-2.0-flash", "gemini-2.5-pro")
|
||||
|
||||
Returns:
|
||||
True if the model supports responseJsonSchema, False otherwise
|
||||
"""
|
||||
model_lower = model.lower()
|
||||
|
||||
# Gemini 2.0+ and 2.5+ models support responseJsonSchema
|
||||
# Pattern matches: gemini-2.0-*, gemini-2.5-*, gemini-3-*, etc.
|
||||
gemini_2_plus_pattern = re.compile(r"gemini-([2-9]|[1-9]\d+)\.")
|
||||
|
||||
return bool(gemini_2_plus_pattern.search(model_lower))
|
||||
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
all_gemini_url_modes = Literal[
|
||||
@@ -514,44 +486,6 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
|
||||
return parameters
|
||||
|
||||
|
||||
def _build_json_schema(parameters: dict) -> dict:
|
||||
"""
|
||||
Build a JSON Schema for use with Gemini's responseJsonSchema parameter.
|
||||
|
||||
Unlike _build_vertex_schema (used for responseSchema), this function:
|
||||
- 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)
|
||||
|
||||
Parameters:
|
||||
parameters: dict - the JSON schema to process
|
||||
|
||||
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)
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
def _filter_anyof_fields(schema_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
When anyof is present, only keep the anyof field and its contents - otherwise VertexAI will throw an error - https://github.com/BerriAI/litellm/issues/11164
|
||||
|
||||
@@ -92,12 +92,7 @@ from litellm.utils import (
|
||||
)
|
||||
|
||||
from ....utils import _remove_additional_properties, _remove_strict_from_schema
|
||||
from ..common_utils import (
|
||||
VertexAIError,
|
||||
_build_json_schema,
|
||||
_build_vertex_schema,
|
||||
supports_response_json_schema,
|
||||
)
|
||||
from ..common_utils import VertexAIError, _build_vertex_schema
|
||||
from ..vertex_llm_base import VertexBase
|
||||
from .transformation import (
|
||||
_gemini_convert_messages_with_history,
|
||||
@@ -629,55 +624,30 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
)
|
||||
return old_schema
|
||||
|
||||
def apply_response_schema_transformation(
|
||||
self, value: dict, optional_params: dict, model: str
|
||||
):
|
||||
def apply_response_schema_transformation(self, value: dict, optional_params: dict):
|
||||
new_value = deepcopy(value)
|
||||
# remove 'strict' from json schema (not supported by Gemini)
|
||||
# remove 'additionalProperties' from json schema
|
||||
new_value = _remove_additional_properties(new_value)
|
||||
# remove 'strict' from json schema
|
||||
new_value = _remove_strict_from_schema(new_value)
|
||||
|
||||
# Automatically use responseJsonSchema for Gemini 2.0+ models
|
||||
# responseJsonSchema uses standard JSON Schema format and supports additionalProperties
|
||||
# For older models (Gemini 1.5), fall back to responseSchema (OpenAPI format)
|
||||
use_json_schema = supports_response_json_schema(model)
|
||||
|
||||
if not use_json_schema:
|
||||
# For responseSchema, remove 'additionalProperties' (not supported)
|
||||
new_value = _remove_additional_properties(new_value)
|
||||
|
||||
# Handle response type
|
||||
if new_value.get("type") == "json_object":
|
||||
if new_value["type"] == "json_object":
|
||||
optional_params["response_mime_type"] = "application/json"
|
||||
elif new_value.get("type") == "text":
|
||||
elif new_value["type"] == "text":
|
||||
optional_params["response_mime_type"] = "text/plain"
|
||||
|
||||
# Extract schema from response_format
|
||||
schema = None
|
||||
if "response_schema" in new_value:
|
||||
optional_params["response_mime_type"] = "application/json"
|
||||
schema = new_value["response_schema"]
|
||||
elif new_value.get("type") == "json_schema":
|
||||
if "json_schema" in new_value and "schema" in new_value["json_schema"]:
|
||||
optional_params["response_schema"] = new_value["response_schema"]
|
||||
elif new_value["type"] == "json_schema": # type: ignore
|
||||
if "json_schema" in new_value and "schema" in new_value["json_schema"]: # type: ignore
|
||||
optional_params["response_mime_type"] = "application/json"
|
||||
schema = new_value["json_schema"]["schema"]
|
||||
optional_params["response_schema"] = new_value["json_schema"]["schema"] # type: ignore
|
||||
|
||||
if schema and isinstance(schema, dict):
|
||||
if use_json_schema:
|
||||
# Use responseJsonSchema (Gemini 2.0+ only, opt-in)
|
||||
# - Standard JSON Schema format (lowercase types)
|
||||
# - Supports additionalProperties
|
||||
# - No propertyOrdering needed
|
||||
optional_params["response_json_schema"] = _build_json_schema(
|
||||
deepcopy(schema)
|
||||
)
|
||||
else:
|
||||
# Use responseSchema (default, backwards compatible)
|
||||
# - OpenAPI-style format (uppercase types)
|
||||
# - No additionalProperties support
|
||||
# - Requires propertyOrdering
|
||||
optional_params["response_schema"] = self._map_response_schema(
|
||||
value=schema
|
||||
)
|
||||
if "response_schema" in optional_params and isinstance(
|
||||
optional_params["response_schema"], dict
|
||||
):
|
||||
optional_params["response_schema"] = self._map_response_schema(
|
||||
value=optional_params["response_schema"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _map_reasoning_effort_to_thinking_budget(
|
||||
@@ -977,7 +947,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
optional_params["max_output_tokens"] = value
|
||||
elif param == "response_format" and isinstance(value, dict): # type: ignore
|
||||
self.apply_response_schema_transformation(
|
||||
value=value, optional_params=optional_params, model=model
|
||||
value=value, optional_params=optional_params
|
||||
)
|
||||
elif param == "frequency_penalty":
|
||||
if self._supports_penalty_parameters(model):
|
||||
|
||||
@@ -207,7 +207,6 @@ class GenerationConfig(TypedDict, total=False):
|
||||
frequency_penalty: float
|
||||
response_mime_type: Literal["text/plain", "application/json"]
|
||||
response_schema: dict
|
||||
response_json_schema: dict
|
||||
seed: int
|
||||
responseLogprobs: bool
|
||||
logprobs: int
|
||||
|
||||
+3
-96
@@ -74,10 +74,6 @@ def test_get_model_name_from_gemini_spec_model():
|
||||
|
||||
|
||||
def test_vertex_ai_response_schema_dict():
|
||||
"""
|
||||
Test that older Gemini models (1.5) use responseSchema (OpenAPI format).
|
||||
responseSchema requires propertyOrdering and doesn't support additionalProperties.
|
||||
"""
|
||||
v = VertexGeminiConfig()
|
||||
non_default_params = {
|
||||
"messages": [{"role": "user", "content": "Hello, world!"}],
|
||||
@@ -113,7 +109,7 @@ def test_vertex_ai_response_schema_dict():
|
||||
transformed_request = v.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params={},
|
||||
model="gemini-1.5-flash", # Old model uses responseSchema (OpenAPI format)
|
||||
model="gemini-2.0-flash-lite",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
@@ -164,9 +160,6 @@ class Step(BaseModel):
|
||||
|
||||
|
||||
def test_vertex_ai_response_schema_defs():
|
||||
"""
|
||||
Test that $defs are unpacked for older Gemini models using responseSchema.
|
||||
"""
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
schema = cast(dict, v.get_json_schema_from_pydantic_object(MathReasoning))
|
||||
@@ -180,7 +173,7 @@ def test_vertex_ai_response_schema_defs():
|
||||
"response_format": schema,
|
||||
},
|
||||
optional_params={},
|
||||
model="gemini-1.5-flash", # Old model uses responseSchema (OpenAPI format)
|
||||
model="gemini-2.0-flash-lite",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
@@ -210,93 +203,7 @@ def test_vertex_ai_response_schema_defs():
|
||||
}
|
||||
|
||||
|
||||
def test_vertex_ai_response_json_schema_for_gemini_2():
|
||||
"""
|
||||
Test that Gemini 2.0+ models automatically use responseJsonSchema.
|
||||
|
||||
responseJsonSchema uses standard JSON Schema format:
|
||||
- lowercase types (string, object, etc.)
|
||||
- no propertyOrdering required
|
||||
- supports additionalProperties
|
||||
"""
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
transformed_request = v.map_openai_params(
|
||||
non_default_params={
|
||||
"messages": [{"role": "user", "content": "Hello, world!"}],
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "test_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optional_params={},
|
||||
model="gemini-2.0-flash", # Gemini 2.0+ automatically uses responseJsonSchema
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Should use response_json_schema, not response_schema
|
||||
assert "response_json_schema" in transformed_request
|
||||
assert "response_schema" not in transformed_request
|
||||
|
||||
# Types should be lowercase (standard JSON Schema format)
|
||||
assert transformed_request["response_json_schema"]["type"] == "object"
|
||||
assert transformed_request["response_json_schema"]["properties"]["name"]["type"] == "string"
|
||||
assert transformed_request["response_json_schema"]["properties"]["age"]["type"] == "integer"
|
||||
|
||||
# Should NOT have propertyOrdering (not needed for responseJsonSchema)
|
||||
assert "propertyOrdering" not in transformed_request["response_json_schema"]
|
||||
|
||||
# additionalProperties should be preserved (supported by responseJsonSchema)
|
||||
assert transformed_request["response_json_schema"].get("additionalProperties") == False
|
||||
|
||||
|
||||
def test_vertex_ai_response_schema_for_old_models():
|
||||
"""
|
||||
Test that older models (Gemini 1.5) automatically use responseSchema.
|
||||
"""
|
||||
v = VertexGeminiConfig()
|
||||
|
||||
transformed_request = v.map_openai_params(
|
||||
non_default_params={
|
||||
"messages": [{"role": "user", "content": "Hello, world!"}],
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "test_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optional_params={},
|
||||
model="gemini-1.5-flash", # Old model automatically uses responseSchema
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Should use response_schema for older models
|
||||
assert "response_schema" in transformed_request
|
||||
assert "response_json_schema" not in transformed_request
|
||||
|
||||
|
||||
def test_vertex_ai_retain_property_ordering():
|
||||
"""
|
||||
Test that existing propertyOrdering is preserved for older models using responseSchema.
|
||||
"""
|
||||
v = VertexGeminiConfig()
|
||||
transformed_request = v.map_openai_params(
|
||||
non_default_params={
|
||||
@@ -317,7 +224,7 @@ def test_vertex_ai_retain_property_ordering():
|
||||
},
|
||||
},
|
||||
optional_params={},
|
||||
model="gemini-1.5-flash", # Old model uses responseSchema which needs propertyOrdering
|
||||
model="gemini-2.0-flash-lite",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user