diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index be8e4749dd8..9f7b7f49a76 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -165,9 +165,18 @@ def _check_text_in_content(parts: List[PartType]) -> bool: return has_text_param -def _build_vertex_schema(parameters: dict): +def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): """ This is a modified version of https://github.com/google-gemini/generative-ai-python/blob/8f77cc6ac99937cd3a81299ecf79608b91b06bbb/google/generativeai/types/content_types.py#L419 + + Updates the input parameters, removing extraneous fields, adjusting types, unwinding $defs, and adding propertyOrdering if specified, returning the updated parameters. + + Parameters: + parameters: dict - the json schema to build from + add_property_ordering: bool - whether to add propertyOrdering to the schema. This is only applicable to schemas for structured outputs. See + set_schema_property_ordering for more details. + Returns: + parameters: dict - the input parameters, modified in place """ # Get valid fields from Schema TypedDict valid_schema_fields = set(get_type_hints(Schema).keys()) @@ -186,8 +195,31 @@ def _build_vertex_schema(parameters: dict): add_object_type(parameters) # Postprocessing # Filter out fields that don't exist in Schema - filtered_parameters = filter_schema_fields(parameters, valid_schema_fields) - return filtered_parameters + parameters = filter_schema_fields(parameters, valid_schema_fields) + + if add_property_ordering: + set_schema_property_ordering(parameters) + return parameters + + +def set_schema_property_ordering(schema: Dict[str, Any]) -> Dict[str, Any]: + """ + vertex ai and generativeai apis order output of fields alphabetically, unless you specify the order. + python dicts retain order, so we just use that. Note that this field only applies to structured outputs, and not tools. + Function tools are not afflicted by the same alphabetical ordering issue, (the order of keys returned seems to be arbitrary, up to the model) + https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.cachedContents#Schema.FIELDS.property_ordering + """ + if "properties" in schema and isinstance(schema["properties"], dict): + # retain propertyOrdering as an escape hatch if user already specifies it + if "propertyOrdering" not in schema: + schema["propertyOrdering"] = [k for k, v in schema["properties"].items()] + for k, v in schema["properties"].items(): + set_schema_property_ordering(v) + if "items" in schema: + set_schema_property_ordering(schema["items"]) + return schema + + def filter_schema_fields( 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 c57090093be..3a58bb2c6d8 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 @@ -207,7 +207,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "extra_headers", "seed", "logprobs", - "top_logprobs", # Added this to list of supported openAI params + "top_logprobs", "modalities", ] @@ -313,9 +313,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if isinstance(old_schema, list): for item in old_schema: if isinstance(item, dict): - item = _build_vertex_schema(parameters=item) + item = _build_vertex_schema(parameters=item, add_property_ordering=True) + elif isinstance(old_schema, dict): - old_schema = _build_vertex_schema(parameters=old_schema) + old_schema = _build_vertex_schema(parameters=old_schema, add_property_ordering=True) return old_schema def apply_response_schema_transformation(self, value: dict, optional_params: dict): diff --git a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index d2169c299e4..41a37e2e579 100644 --- a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -9,6 +9,8 @@ from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( VertexGeminiConfig, ) from litellm.types.utils import ChoiceLogprobs +from pydantic import BaseModel +from typing import List, cast def test_top_logprobs(): @@ -62,3 +64,160 @@ def test_get_model_name_from_gemini_spec_model(): model = "gemini/ft-uuid-123" result = VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) assert result == "ft-uuid-123" + + + +def test_vertex_ai_response_schema_dict(): + 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": "math_reasoning", + "schema": { + "type": "object", + "properties": { + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "thought": {"type": "string"}, + "output": {"type": "string"}, + }, + "required": ["thought", "output"], + "additionalProperties": False, + }, + }, + "final_answer": {"type": "string"}, + }, + "required": ["steps", "final_answer"], + "additionalProperties": False, + }, + "strict": False, + }, + }, + }, + optional_params={}, + model="gemini-2.0-flash-lite", + drop_params=False, + ) + + schema = transformed_request["response_schema"] + # should add propertyOrdering + assert schema["propertyOrdering"] == ["steps", "final_answer"] + # should add propertyOrdering (recursively, including array items) + assert schema["properties"]["steps"]["items"]["propertyOrdering"] == [ + "thought", + "output", + ] + # should strip strict and additionalProperties + assert "strict" not in schema + assert "additionalProperties" not in schema + # validate the whole thing to catch regressions + assert transformed_request["response_schema"] == { + "type": "object", + "properties": { + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "thought": {"type": "string"}, + "output": {"type": "string"}, + }, + "required": ["thought", "output"], + "propertyOrdering": ["thought", "output"], + }, + }, + "final_answer": {"type": "string"}, + }, + "required": ["steps", "final_answer"], + "propertyOrdering": ["steps", "final_answer"], + } + + +class MathReasoning(BaseModel): + steps: List["Step"] + final_answer: str + + +class Step(BaseModel): + thought: str + output: str + + +def test_vertex_ai_response_schema_defs(): + v = VertexGeminiConfig() + + schema = cast(dict, v.get_json_schema_from_pydantic_object(MathReasoning)) + + # pydantic conversion by default adds $defs to the schema, make sure this is still the case, otherwise this test isn't really testing anything + 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.0-flash-lite", + drop_params=False, + ) + + assert "$defs" not in transformed_request["response_schema"] + assert transformed_request["response_schema"] == { + "title": "MathReasoning", + "type": "object", + "properties": { + "steps": { + "title": "Steps", + "type": "array", + "items": { + "title": "Step", + "type": "object", + "properties": { + "thought": {"title": "Thought", "type": "string"}, + "output": {"title": "Output", "type": "string"}, + }, + "required": ["thought", "output"], + "propertyOrdering": ["thought", "output"], + }, + }, + "final_answer": {"title": "Final Answer", "type": "string"}, + }, + "required": ["steps", "final_answer"], + "propertyOrdering": ["steps", "final_answer"], + } + + +def test_vertex_ai_retain_property_ordering(): + 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": "math_reasoning", + "schema": { + "type": "object", + "properties": { + "output": {"type": "string"}, + "thought": {"type": "string"}, + }, + "propertyOrdering": ["thought", "output"], + }, + }, + }, + }, + optional_params={}, + model="gemini-2.0-flash-lite", + drop_params=False, + ) + + schema = transformed_request["response_schema"] + # should leave existing value alone, despite dictionary ordering + assert schema["propertyOrdering"] == ["thought", "output"]