fix(vertex): sanitise empty-string enums under anyOf

_fix_enum_empty_strings recursed into properties and items but not anyOf.
convert_anyof_null_to_nullable runs first, so an Optional[Literal["", ...]]
field is already an anyOf branch by the time the sanitiser sees it - the empty
string survived on optional fields while the identical enum was fixed on
required ones, and went out in the functionDeclarations payload.

Adds the anyOf arm, mirroring _fix_enum_types, plus the same isinstance guard
that function already has (without it a non-dict branch would raise).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ambuj Upadhyay 2026-09-06 07:36:45 +05:30 • committed by Ambuj Upadhyay
parent 14f4c34c61
commit 5be6a018ce
2 changed files with 75 additions and 0 deletions

View file

@ -620,6 +620,9 @@ def _fix_enum_empty_strings(schema, depth=0):
if depth > DEFAULT_MAX_RECURSE_DEPTH:
raise ValueError(f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema.")
if not isinstance(schema, dict):
return
if "enum" in schema and isinstance(schema["enum"], list):
schema["enum"] = [None if value == "" else value for value in schema["enum"]]
@ -633,6 +636,14 @@ def _fix_enum_empty_strings(schema, depth=0):
if items is not None:
_fix_enum_empty_strings(items, depth=depth + 1)
# An Optional[Literal[...]] arrives here as an `anyOf` branch, because
# convert_anyof_null_to_nullable runs first. Without this arm the empty string
# survives on optional fields while the same enum is fixed on required ones.
anyof: Final = schema.get("anyOf", None)
if anyof is not None and isinstance(anyof, list):
for item in anyof:
_fix_enum_empty_strings(item, depth=depth + 1)
def _fix_enum_types(schema, depth=0):
"""Remove `enum` fields when the schema type is not string.

View file

@ -1755,3 +1755,67 @@ 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_fix_enum_empty_strings_nullable_enum():
"""An Optional[Literal["", ...]] field must be sanitised like a required one.
convert_anyof_null_to_nullable runs before _fix_enum_empty_strings, so a nullable
enum has already become an `anyOf` branch by the time the sanitiser sees it. Without
recursion into `anyOf` the empty string survives on the optional field only.
"""
from litellm.llms.vertex_ai.common_utils import _fix_enum_empty_strings
schema = {
"type": "object",
"properties": {
"mode": {"anyOf": [{"type": "string", "enum": ["", "fast", "slow"]}, {"type": "null"}]},
"plain_mode": {"type": "string", "enum": ["", "fast", "slow"]},
},
}
_fix_enum_empty_strings(schema)
assert schema["properties"]["mode"]["anyOf"][0]["enum"] == [None, "fast", "slow"]
assert schema["properties"]["plain_mode"]["enum"] == [None, "fast", "slow"]
def test_fix_enum_empty_strings_anyof_without_a_null_branch():
"""The gap is not limited to nullable fields - any enum under `anyOf` was missed."""
from litellm.llms.vertex_ai.common_utils import _fix_enum_empty_strings
schema = {
"type": "object",
"properties": {"m": {"anyOf": [{"type": "string", "enum": ["", "a"]}, {"type": "integer"}]}},
}
_fix_enum_empty_strings(schema)
assert schema["properties"]["m"]["anyOf"][0]["enum"] == [None, "a"]
def test_fix_enum_empty_strings_tolerates_a_non_dict_anyof_branch():
"""A malformed branch must not raise, matching _fix_enum_types' isinstance guard."""
from litellm.llms.vertex_ai.common_utils import _fix_enum_empty_strings
schema = {"anyOf": ["string", {"type": "string", "enum": ["", "a"]}]}
_fix_enum_empty_strings(schema)
assert schema["anyOf"][1]["enum"] == [None, "a"]
def test_build_vertex_schema_sanitises_a_nullable_enum_end_to_end():
"""The whole pipeline, as a tool schema reaches it from pydantic."""
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
parameters = {
"type": "object",
"properties": {
"mode": {"anyOf": [{"type": "string", "enum": ["", "fast", "slow"]}, {"type": "null"}]},
"plain_mode": {"type": "string", "enum": ["", "fast", "slow"]},
},
}
built = _build_vertex_schema(parameters)
assert "" not in built["properties"]["mode"]["anyOf"][0]["enum"]
assert "" not in built["properties"]["plain_mode"]["enum"]