From 216f818464706b5b7f5ca704cad436d40d1e6334 Mon Sep 17 00:00:00 2001 From: Ben Clarke Date: Sat, 15 Aug 2026 22:46:04 +0100 Subject: [PATCH] fix: preserve anyOf in LiteLLM tool schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_schema_to_dict` builds its dict with `model_dump()`, which emits the pydantic field name `any_of`, and recurses only into `items` and `properties`. A union therefore leaves ADK spelled `any_of` with genai's uppercase type enums inside, LiteLLM's provider transforms drop the unrecognised key, and the argument reaches the model as a bare `{"type": "object"}` with none of its variants. - Rename `any_of` to `anyOf` and recurse into its branches - Accept an already-camelCase `anyOf` from dict input A discriminated union of object variants loses every variant, every discriminator value and every nested description this way, and nothing raises: the model is handed a schema that does not describe the argument. Affects any tool with a union in its parameters — OpenAPI toolsets, MCP tools, client-side tools — on every provider routed through LiteLLM. The native path is unaffected, because `types.Schema` serializes by alias. Fixes #6738 --- src/google/adk/models/lite_llm.py | 15 +++++++ tests/unittests/models/test_litellm.py | 61 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index a854c0104b..b96a9629d1 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -2061,6 +2061,21 @@ def _schema_to_dict(schema: types.Schema | dict[str, Any]) -> dict[str, Any]: else items ) + # `model_dump()` spells this `any_of`, but downstream JSON Schema consumers + # read `anyOf`, so an un-renamed union is silently dropped: the argument + # reaches the model as a bare `{"type": "object"}` with none of its variants. + # Recursing also lowercases the types nested inside each branch. + any_of = schema_dict.pop("any_of", None) + if any_of is None: + any_of = schema_dict.get("anyOf") + if any_of is not None: + schema_dict["anyOf"] = [ + _schema_to_dict(item) + if isinstance(item, (types.Schema, dict)) + else item + for item in any_of + ] + if "properties" in schema_dict: new_props = {} for key, value in schema_dict["properties"].items(): diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 68b0e74c17..b70246ff2b 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -805,6 +805,67 @@ def test_schema_to_dict_filters_none_enum_values(): ] +def test_schema_to_dict_preserves_any_of_as_camel_case(): + schema = types.Schema( + type=types.Type.OBJECT, + properties={ + "value": types.Schema( + any_of=[ + types.Schema(type=types.Type.STRING), + types.Schema(type=types.Type.NUMBER), + ], + description="id or index", + ), + }, + ) + + value = _schema_to_dict(schema)["properties"]["value"] + + assert "any_of" not in value + assert value["anyOf"] == [{"type": "string"}, {"type": "number"}] + assert value["description"] == "id or index" + + +def test_schema_to_dict_recurses_into_any_of_branches(): + """A union of object variants keeps its branches, nested types and all.""" + schema = types.Schema( + type=types.Type.ARRAY, + items=types.Schema( + any_of=[ + types.Schema( + type=types.Type.OBJECT, + properties={ + "type": types.Schema( + type=types.Type.STRING, enum=["table"] + ), + "rows": types.Schema( + type=types.Type.ARRAY, + items=types.Schema(type=types.Type.STRING), + ), + }, + ), + types.Schema( + type=types.Type.OBJECT, + properties={ + "type": types.Schema( + type=types.Type.STRING, enum=["divider"] + ), + }, + ), + ], + ), + ) + + branches = _schema_to_dict(schema)["items"]["anyOf"] + + assert [branch["properties"]["type"]["enum"] for branch in branches] == [ + ["table"], + ["divider"], + ] + assert branches[0]["type"] == "object" + assert branches[0]["properties"]["rows"]["items"]["type"] == "string" + + def test_safe_json_serialize_serializable_object(): assert _safe_json_serialize({"a": 1, "b": [2, 3]}) == '{"a": 1, "b": [2, 3]}'