From bcfe5b7de6ba1cfc7c86d873e14c9e910f104c3e Mon Sep 17 00:00:00 2001 From: Mohammad Ali Farhan Date: Sat, 8 Aug 2026 03:08:00 +0530 Subject: [PATCH] fix(vertex_ai): honor Anthropic-shaped input_schema on Gemini tools _map_function only ever read a tool's schema from `parameters`, so a tool carrying it under Anthropic's `input_schema` key reached Gemini as a FunctionDeclaration with a name and description but no parameters at all. The request still returned 200 and the model still "called" the function, just with empty arguments, so the loss was completely silent. Fall back to `input_schema` when `parameters` is absent, running it through _build_vertex_schema like the OpenAI path does. That conversion matters: an Anthropic schema is JSON Schema, so without it $defs/$ref reach Vertex raw and get rejected. Bedrock and Databricks already accept either shape, and the Anthropic adapters translate the key before the provider config sees it, so Gemini was the outlier. The gap is reachable whenever a tool list is shared across providers or a router fallback lands on Gemini. Fixes #35685 --- .../vertex_and_google_ai_studio_gemini.py | 9 +- ...test_vertex_and_google_ai_studio_gemini.py | 88 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index ff51f1a013e..c116960a1ff 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -604,7 +604,14 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): openai_function_object = _openai_function_object elif "name" in tool: # functions list - openai_function_object = ChatCompletionToolParamFunctionChunk(**tool) + _named_function_object = ChatCompletionToolParamFunctionChunk(**tool) + + if _named_function_object.get("parameters") is None: + _input_schema = tool.get("input_schema") + if isinstance(_input_schema, dict): + _named_function_object["parameters"] = _build_vertex_schema(_input_schema) + + openai_function_object = _named_function_object if "type" in tool and tool["type"] == "computer_use": computer_use_config = {k: v for k, v in tool.items() if k != "type"} diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 51cc2857252..55559b441ba 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -1182,6 +1182,94 @@ def test_vertex_ai_map_tool_with_anyof(): }, f"Expected only anyOf field and its contents to be kept, but got {new_tools[0]['function_declarations'][0]['parameters']['properties']['base_branch']}" +def _anthropic_shaped_tool(input_schema: dict) -> dict: + return { + "name": "get_weather", + "description": "Get the current weather for a location.", + "input_schema": input_schema, + } + + +def _declaration_for(tool: dict) -> dict: + """Map a single tool through the public entry point and return its FunctionDeclaration.""" + transformed = VertexGeminiConfig().map_openai_params( + non_default_params={"tools": [deepcopy(tool)]}, + optional_params={}, + model="gemini-2.5-flash", + drop_params=False, + ) + return transformed["tools"][0]["function_declarations"][0] + + +def test_vertex_ai_map_tool_with_anthropic_input_schema(): + """ + Related issue: https://github.com/BerriAI/litellm/issues/35685 + + A tool that carries its schema under Anthropic's `input_schema` key instead of + OpenAI's `parameters` must still reach Gemini with its parameters. Dropping them + produces an HTTP 200 and a tool call with empty arguments, so the failure is + silent and the model never sees the schema it needed. + """ + declaration = _declaration_for( + _anthropic_shaped_tool( + { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + } + ) + ) + + assert declaration["name"] == "get_weather" + assert declaration.get("parameters") == { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, f"input_schema was dropped from the Gemini FunctionDeclaration: {declaration}" + + +def test_vertex_ai_map_tool_input_schema_gets_vertex_schema_conversion(): + """ + `input_schema` is JSON Schema just like `parameters`, so it needs the same + OpenAPI conversion. Passing it through raw leaves `$defs`/`$ref` in place, which + Vertex rejects. + """ + declaration = _declaration_for( + _anthropic_shaped_tool( + { + "type": "object", + "properties": {"loc": {"$ref": "#/$defs/Loc"}}, + "$defs": {"Loc": {"type": "object", "properties": {"city": {"type": "string"}}}}, + } + ) + ) + + parameters = declaration["parameters"] + assert "$defs" not in parameters + assert parameters["properties"]["loc"] == { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, f"$ref was not unwound: {parameters}" + + +def test_vertex_ai_map_tool_explicit_parameters_wins_over_input_schema(): + """A tool carrying both keys must keep `parameters` — `input_schema` is only a fallback.""" + tool = _anthropic_shaped_tool({"type": "object", "properties": {"ignored": {"type": "string"}}}) + tool["parameters"] = {"type": "object", "properties": {"location": {"type": "string"}}} + + declaration = _declaration_for(tool) + + assert declaration["parameters"]["properties"] == {"location": {"type": "string"}} + + +def test_vertex_ai_map_tool_without_any_schema_is_unchanged(): + """A named tool with no schema at all must still map, without inventing parameters.""" + declaration = _declaration_for({"name": "ping", "description": "no args"}) + + assert declaration["name"] == "ping" + assert "parameters" not in declaration + + def test_vertex_ai_streaming_usage_calculation(): """ Ensure streaming usage calculation uses same function as non-streaming usage calculation