mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
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:
parent
1a54e47639
commit
bcfe5b7de6
2 changed files with 96 additions and 1 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue