Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion docs/my-website/docs/completion/json_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,93 @@ curl http://0.0.0.0:4000/v1/chat/completions \
```

</TabItem>
</Tabs>
</Tabs>

## Gemini - Use Native JSON Schema Format (Gemini 2.0+)

Gemini 2.0+ models support a native `responseJsonSchema` parameter that uses standard JSON Schema format. This provides better compatibility with Pydantic schemas and supports `additionalProperties`.

### Benefits of `use_json_schema: True`:
- 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 # Now works with use_json_schema!
}
},
"use_json_schema": True # Opt-in to native JSON Schema format
}
)
```

</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
}
},
"use_json_schema": true
}
}'
```

</TabItem>
</Tabs>

### Supported Models

`use_json_schema: True` is supported on Gemini 2.0+ models:
- `gemini-2.0-flash`
- `gemini-2.0-flash-lite`
- `gemini-2.5-pro`
- `gemini-2.5-flash`

For older models (e.g., `gemini-1.5-flash`), the parameter is ignored and falls back to the default `responseSchema` format.
66 changes: 66 additions & 0 deletions litellm/llms/vertex_ai/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ 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[
Expand Down Expand Up @@ -467,6 +495,44 @@ 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
Expand Down
74 changes: 55 additions & 19 deletions litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@
)

from ....utils import _remove_additional_properties, _remove_strict_from_schema
from ..common_utils import VertexAIError, _build_vertex_schema
from ..common_utils import (
VertexAIError,
_build_json_schema,
_build_vertex_schema,
supports_response_json_schema,
)
from ..vertex_llm_base import VertexBase
from .transformation import (
_gemini_convert_messages_with_history,
Expand Down Expand Up @@ -595,30 +600,61 @@ def _map_response_schema(self, value: dict) -> dict:
)
return old_schema

def apply_response_schema_transformation(self, value: dict, optional_params: dict):
def apply_response_schema_transformation(
self, value: dict, optional_params: dict, model: str
):
new_value = deepcopy(value)
# remove 'additionalProperties' from json schema
new_value = _remove_additional_properties(new_value)
# remove 'strict' from json schema
# remove 'strict' from json schema (not supported by Gemini)
new_value = _remove_strict_from_schema(new_value)
if new_value["type"] == "json_object":

# Check if user explicitly opted-in to use responseJsonSchema
# This is opt-in to maintain backwards compatibility
use_json_schema = new_value.pop("use_json_schema", False)

# Only allow use_json_schema for models that support it (Gemini 2.0+)
if use_json_schema and not supports_response_json_schema(model):
verbose_logger.warning(
f"Model {model} does not support responseJsonSchema. Falling back to responseSchema."
)
use_json_schema = False

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":
optional_params["response_mime_type"] = "application/json"
elif new_value["type"] == "text":
elif new_value.get("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"
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
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_mime_type"] = "application/json"
optional_params["response_schema"] = new_value["json_schema"]["schema"] # type: ignore

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"]
)
schema = new_value["json_schema"]["schema"]

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
)

@staticmethod
def _map_reasoning_effort_to_thinking_budget(
Expand Down Expand Up @@ -868,7 +904,7 @@ def map_openai_params( # noqa: PLR0915
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
value=value, optional_params=optional_params, model=model
)
elif param == "frequency_penalty":
if self._supports_penalty_parameters(model):
Expand Down
1 change: 1 addition & 0 deletions litellm/types/llms/vertex_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,91 @@ def test_vertex_ai_response_schema_defs():
}


def test_vertex_ai_response_json_schema_opt_in():
"""
Test that use_json_schema=True uses responseJsonSchema for Gemini 2.0+ models.

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,
},
},
"use_json_schema": True, # Opt-in to responseJsonSchema
},
},
optional_params={},
model="gemini-2.0-flash",
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_json_schema_fallback_for_old_models():
"""
Test that use_json_schema=True falls back to responseSchema for older models.
"""
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"},
},
},
},
"use_json_schema": True, # Opt-in, but model doesn't support it
},
},
optional_params={},
model="gemini-1.5-flash", # Old model, doesn't support responseJsonSchema
drop_params=False,
)

# Should fall back to 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():
v = VertexGeminiConfig()
transformed_request = v.map_openai_params(
Expand Down
Loading