diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index fbe6ab35edf..f292e5f6802 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -712,6 +712,20 @@ def convert_anyof_null_to_nullable(schema, depth=0): ) if contains_null: + # Drop any-type schemas (bare {}) from anyOf before adding nullable=True. + # Adding nullable=True to {} produces {"nullable": True} with no type field, + # which Gemini rejects as an anyOf entry without a concrete type. + for atype in list(anyof): + if isinstance(atype, dict) and _is_any_type_schema(atype): + anyof.remove(atype) + + if len(anyof) == 0: + # All remaining entries were any-type schemas (e.g. anyOf: [{}, null]). + # This means "any nullable value" — collapse anyOf and mark parent nullable. + del schema["anyOf"] + schema["nullable"] = True + return + # set all types to nullable following guidance found here: https://cloud.google.com/vertex-ai/generative-ai/docs/samples/generativeaionvertexai-gemini-controlled-generation-response-schema-3#generativeaionvertexai_gemini_controlled_generation_response_schema_3-python for atype in anyof: # Remove items field if type is array and items is empty diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c75a67cfc19..900894f74d6 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -2110,7 +2110,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/eu/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -2143,7 +2144,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/eu/gpt-5.1-codex": { "cache_read_input_token_cost": 1.4e-07, @@ -2410,7 +2412,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/global/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -2443,7 +2446,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/global/gpt-5.1-codex": { "cache_read_input_token_cost": 1.25e-07, @@ -3456,7 +3460,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_service_tier": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/gpt-5.1-chat-2025-11-13": { "cache_read_input_token_cost": 1.25e-07, @@ -3491,7 +3496,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": false, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/gpt-5.1-codex-2025-11-13": { "cache_read_input_token_cost": 1.25e-07, @@ -3906,7 +3912,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -3939,7 +3946,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/gpt-5.1-codex": { "cache_read_input_token_cost": 1.25e-07, @@ -5273,7 +5281,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/us/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -5306,7 +5315,8 @@ "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_none_reasoning_effort": true }, "azure/us/gpt-5.1-codex": { "cache_read_input_token_cost": 1.4e-07, @@ -21068,18 +21078,18 @@ "input_cost_per_token_flex": 1.5e-05, "input_cost_per_token_batches": 1.5e-05, "input_cost_per_token_priority": 6e-05, - "input_cost_per_token_above_272k_tokens_priority": 1.2e-04, + "input_cost_per_token_above_272k_tokens_priority": 0.00012, "litellm_provider": "openai", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 1.8e-04, - "output_cost_per_token_above_272k_tokens": 2.7e-04, + "output_cost_per_token": 0.00018, + "output_cost_per_token_above_272k_tokens": 0.00027, "output_cost_per_token_flex": 9e-05, "output_cost_per_token_batches": 9e-05, - "output_cost_per_token_priority": 2.7e-04, - "output_cost_per_token_above_272k_tokens_priority": 4.05e-04, + "output_cost_per_token_priority": 0.00027, + "output_cost_per_token_above_272k_tokens_priority": 0.000405, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -21117,18 +21127,18 @@ "input_cost_per_token_flex": 1.5e-05, "input_cost_per_token_batches": 1.5e-05, "input_cost_per_token_priority": 6e-05, - "input_cost_per_token_above_272k_tokens_priority": 1.2e-04, + "input_cost_per_token_above_272k_tokens_priority": 0.00012, "litellm_provider": "openai", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 1.8e-04, - "output_cost_per_token_above_272k_tokens": 2.7e-04, + "output_cost_per_token": 0.00018, + "output_cost_per_token_above_272k_tokens": 0.00027, "output_cost_per_token_flex": 9e-05, "output_cost_per_token_batches": 9e-05, - "output_cost_per_token_priority": 2.7e-04, - "output_cost_per_token_above_272k_tokens_priority": 4.05e-04, + "output_cost_per_token_priority": 0.00027, + "output_cost_per_token_above_272k_tokens_priority": 0.000405, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index b80aa996cae..0bd7f41676d 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -1451,11 +1451,15 @@ def test_add_object_type_preserves_any_type_schema(): assert schema["type"] == "object" -def test_convert_anyof_preserves_any_type_members(): - """Test convert_anyof_null_to_nullable does NOT coerce empty anyOf members to object.""" +def test_convert_anyof_pure_any_type_collapses_to_nullable_parent(): + """ + anyOf: [{}, null] means "any nullable value". + The empty schema {} (any-type) inside anyOf cannot receive nullable=True and be + sent to Gemini — Gemini rejects {"nullable": True} with no type field as an anyOf + entry. Instead we collapse the anyOf and set nullable=True on the parent. + """ from litellm.llms.vertex_ai.common_utils import convert_anyof_null_to_nullable - # anyOf with empty schema and null — empty should be preserved schema = { "anyOf": [ {}, @@ -1463,10 +1467,42 @@ def test_convert_anyof_preserves_any_type_members(): ] } convert_anyof_null_to_nullable(schema) - # null should be removed, empty schema should be preserved (not coerced to object) - assert len(schema["anyOf"]) == 1 - assert "type" not in schema["anyOf"][0] or schema["anyOf"][0].get("type") != "object" - assert schema["anyOf"][0].get("nullable") is True + # anyOf should be removed; parent gets nullable=True to represent "any nullable value" + assert "anyOf" not in schema + assert schema.get("nullable") is True + + +def test_convert_anyof_drops_empty_schema_when_concrete_type_present(): + """ + Regression test for tool schemas with anyOf: [array, {}, null]. + The bare {} (any-type) inside anyOf must be dropped — Gemini rejects + {"nullable": True} with no type field. The concrete array type should + remain with nullable=True. + + Broken by da941e4261c5 (preserve type schema semantics for JsonValue fields). + """ + from litellm.llms.vertex_ai.common_utils import convert_anyof_null_to_nullable + + schema = { + "properties": { + "callbacks": { + "anyOf": [ + {"items": {}, "type": "array"}, + {}, + {"type": "null"}, + ] + } + } + } + convert_anyof_null_to_nullable(schema) + + callbacks_anyof = schema["properties"]["callbacks"]["anyOf"] + # {} and {"type": "null"} should both be gone; only the array entry remains + assert len(callbacks_anyof) == 1 + assert callbacks_anyof[0].get("type") == "array" + assert callbacks_anyof[0].get("nullable") is True + # empty items should have been stripped too + assert "items" not in callbacks_anyof[0] def test_build_vertex_schema_jsonvalue():