From 836e6cbafee23cb7a3013635a6897bb3d299f0d8 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Sun, 27 Sep 2026 04:08:45 +0530 Subject: [PATCH 1/3] fix(vertex): keep scalar constraints on type-array anyOf branches --- litellm/llms/vertex_ai/common_utils.py | 25 +++++++++++- .../vertex_ai/test_vertex_ai_common_utils.py | 39 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 6d050d5a856..1fa918d976a 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -1015,6 +1015,19 @@ def _convert_schema_types(schema, depth=0): "minProperties", "maxProperties", } + # Constraint keywords that apply to primitive types and should move + # into the anyOf branch with the type. Without this they are left on + # the parent next to anyOf and dropped by _filter_anyof_fields. + scalar_constraint_fields: Final = { + "enum", + "pattern", + "minLength", + "maxLength", + "minimum", + "maximum", + "multipleOf", + "format", + } any_of: Final[list[dict[str, object]]] = [] for t in type_val: @@ -1034,14 +1047,22 @@ def _convert_schema_types(schema, depth=0): item_schema[field] = deepcopy(schema[field]) any_of.append(item_schema) else: - # For primitive types, only include the type - any_of.append({"type": t}) + # For primitive types, carry the scalar constraint keywords + # into the branch so they survive _filter_anyof_fields + item_schema: dict[str, object] = {"type": t} + for field in scalar_constraint_fields: + if field in schema: + item_schema[field] = deepcopy(schema[field]) + any_of.append(item_schema) # Remove type-specific fields from parent if we moved them into anyOf has_object_or_array: Final = any(t in ("object", "array") for t in type_val if isinstance(t, str)) if has_object_or_array: for field in type_specific_fields: schema.pop(field, None) + else: + for field in scalar_constraint_fields: + schema.pop(field, None) schema["anyOf"] = any_of schema.pop("type") diff --git a/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py index 04a7ee451c4..b382a62013c 100644 --- a/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -1755,3 +1755,42 @@ def test_get_vertex_ai_lyria_model_info_is_none_for_non_lyria_speech_models(mode assert get_vertex_ai_lyria_model_info(model=model) is None + +def test_type_array_keeps_scalar_constraints_on_anyof_branches(): + """A nullable type array must not lose enum/min-max/pattern/format (issue #43325).""" + from litellm.llms.vertex_ai.common_utils import _build_vertex_schema + + schema = { + "type": "object", + "properties": { + "status": {"type": ["string", "null"], "enum": ["open", "closed"], "maxLength": 6}, + "priority": {"type": ["integer", "null"], "minimum": 1, "maximum": 5}, + "code": {"type": ["string", "null"], "pattern": "^[A-Z]+$", "format": "date-time"}, + }, + "required": ["status"], + } + + assert _build_vertex_schema(schema) == { + "type": "object", + "properties": { + "status": { + "anyOf": [ + {"type": "string", "enum": ["open", "closed"], "maxLength": 6}, + {"type": "null"}, + ] + }, + "priority": { + "anyOf": [ + {"type": "integer", "minimum": 1, "maximum": 5}, + {"type": "null"}, + ] + }, + "code": { + "anyOf": [ + {"type": "string", "pattern": "^[A-Z]+$", "format": "date-time"}, + {"type": "null"}, + ] + }, + }, + "required": ["status"], + } From 6f2fb389a16a99944bf8f26d3f36fca59a67e96d Mon Sep 17 00:00:00 2001 From: Azhar Date: Sun, 27 Sep 2026 10:22:32 +0530 Subject: [PATCH 2/3] fix(vertex): avoid reportRedeclaration in type-array scalar constraint move --- litellm/llms/vertex_ai/common_utils.py | 36 ++++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 1fa918d976a..49362bfba31 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -1038,31 +1038,21 @@ def _convert_schema_types(schema, depth=0): any_of.append({"type": "null"}) continue - # For object/array types, include type-specific fields - if t in ("object", "array"): - item_schema: dict[str, object] = {"type": t} - # Move type-specific fields into this anyOf item - for field in type_specific_fields: - if field in schema: - item_schema[field] = deepcopy(schema[field]) - any_of.append(item_schema) - else: - # For primitive types, carry the scalar constraint keywords - # into the branch so they survive _filter_anyof_fields - item_schema: dict[str, object] = {"type": t} - for field in scalar_constraint_fields: - if field in schema: - item_schema[field] = deepcopy(schema[field]) - any_of.append(item_schema) + item_schema: dict[str, object] = {"type": t} + # For object/array types, move the type-specific fields into + # the branch; for primitives, carry the scalar constraint + # keywords so they survive _filter_anyof_fields. + branch_fields = type_specific_fields if t in ("object", "array") else scalar_constraint_fields + for field in branch_fields: + if field in schema: + item_schema[field] = deepcopy(schema[field]) + any_of.append(item_schema) - # Remove type-specific fields from parent if we moved them into anyOf + # Remove the fields we moved into anyOf branches from the parent has_object_or_array: Final = any(t in ("object", "array") for t in type_val if isinstance(t, str)) - if has_object_or_array: - for field in type_specific_fields: - schema.pop(field, None) - else: - for field in scalar_constraint_fields: - schema.pop(field, None) + parent_fields = type_specific_fields if has_object_or_array else scalar_constraint_fields + for field in parent_fields: + schema.pop(field, None) schema["anyOf"] = any_of schema.pop("type") From e5bd4102515ac7de52b8608b45511de5c7936e97 Mon Sep 17 00:00:00 2001 From: Azhar Date: Tue, 29 Sep 2026 04:04:53 +0530 Subject: [PATCH 3/3] fix(vertex): attach enum to string branch only, drop multipleOf copy --- litellm/llms/vertex_ai/common_utils.py | 17 ++++++++-- .../vertex_ai/test_vertex_ai_common_utils.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 49362bfba31..d4102ff0989 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -1018,6 +1018,8 @@ def _convert_schema_types(schema, depth=0): # Constraint keywords that apply to primitive types and should move # into the anyOf branch with the type. Without this they are left on # the parent next to anyOf and dropped by _filter_anyof_fields. + # multipleOf is not copied on purpose: filter_schema_fields drops it + # from the branch anyway, so carrying it would only imply it survives. scalar_constraint_fields: Final = { "enum", "pattern", @@ -1025,7 +1027,6 @@ def _convert_schema_types(schema, depth=0): "maxLength", "minimum", "maximum", - "multipleOf", "format", } @@ -1044,8 +1045,18 @@ def _convert_schema_types(schema, depth=0): # keywords so they survive _filter_anyof_fields. branch_fields = type_specific_fields if t in ("object", "array") else scalar_constraint_fields for field in branch_fields: - if field in schema: - item_schema[field] = deepcopy(schema[field]) + if field not in schema: + continue + if field == "enum": + # enums only land on the string branch, string values + # only: a mixed enum like ["a", 1] must not put 1 on + # the string branch. _fix_enum_types drops the rest. + if t == "string" and isinstance(schema["enum"], list): + string_values = [v for v in schema["enum"] if isinstance(v, str)] + if string_values: + item_schema["enum"] = string_values + continue + item_schema[field] = deepcopy(schema[field]) any_of.append(item_schema) # Remove the fields we moved into anyOf branches from the parent diff --git a/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py index b382a62013c..e0645630ba7 100644 --- a/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/unit/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -1794,3 +1794,34 @@ def test_type_array_keeps_scalar_constraints_on_anyof_branches(): }, "required": ["status"], } + + +def test_type_array_mixed_enum_only_string_values_on_string_branch(): + """A mixed-type enum must not leak non-string values onto the string branch.""" + from litellm.llms.vertex_ai.common_utils import _build_vertex_schema + + schema = { + "type": "object", + "properties": { + "mixed": {"type": ["string", "integer"], "enum": ["a", 1]}, + "numbers": {"type": ["string", "integer"], "enum": [1, 2]}, + }, + } + + assert _build_vertex_schema(schema) == { + "type": "object", + "properties": { + "mixed": { + "anyOf": [ + {"type": "string", "enum": ["a"]}, + {"type": "integer"}, + ] + }, + "numbers": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + }, + }