mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
14f4c34c61
commit
5be6a018ce
2 changed files with 75 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue