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
This commit is contained in:
Mohammad Ali Farhan 2026-08-08 03:08:00 +05:30
parent 1a54e47639
commit bcfe5b7de6
2 changed files with 96 additions and 1 deletions

View file

@ -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"}

View file

@ -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