Skip to content

Commit c200d1e

Browse files
committed
fix(litellm): collect all tools so native tools aren't dropped
_get_completion_inputs only converted tools[0].function_declarations, so Tools carrying only native/built-in tools (e.g. google_search) produced tools=None and any Tool past index 0 was ignored entirely. Loop over all config.tools instead: convert function declarations as before and serialize native tools via model_dump(by_alias=True, exclude_none=True) into the same list. Apply the same fix to the tools[0]-only slice in _build_request_log. Fixes #6091
1 parent fd006db commit c200d1e

2 files changed

Lines changed: 121 additions & 14 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import sys
2828
from typing import Any
2929
from typing import AsyncGenerator
30-
from typing import cast
3130
from typing import Dict
3231
from typing import Generator
3332
from typing import Iterable
@@ -2376,15 +2375,22 @@ async def _get_completion_inputs(
23762375

23772376
# 2. Convert tool declarations
23782377
tools: Optional[List[Dict[str, Any]]] = None
2379-
if (
2380-
llm_request.config
2381-
and llm_request.config.tools
2382-
and llm_request.config.tools[0].function_declarations
2383-
):
2384-
tools = [
2385-
_function_declaration_to_tool_param(tool)
2386-
for tool in llm_request.config.tools[0].function_declarations
2387-
]
2378+
if llm_request.config and llm_request.config.tools:
2379+
tools = []
2380+
for tool in llm_request.config.tools:
2381+
if not isinstance(tool, types.Tool):
2382+
continue
2383+
if tool.function_declarations:
2384+
tools.extend(
2385+
_function_declaration_to_tool_param(func_decl)
2386+
for func_decl in tool.function_declarations
2387+
)
2388+
else:
2389+
# Native/built-in tools (e.g. google_search) carry no
2390+
# function_declarations; serialize them as-is so they reach the
2391+
# provider or proxy instead of being silently dropped.
2392+
tools.append(tool.model_dump(by_alias=True, exclude_none=True))
2393+
tools = tools or None
23882394

23892395
# 3. Handle response format
23902396
response_format: dict[str, Any] | None = None
@@ -2458,10 +2464,12 @@ def _build_request_log(req: LlmRequest) -> str:
24582464
The request log.
24592465
"""
24602466

2461-
function_decls: list[types.FunctionDeclaration] = cast(
2462-
list[types.FunctionDeclaration],
2463-
req.config.tools[0].function_declarations if req.config.tools else [],
2464-
)
2467+
function_decls: list[types.FunctionDeclaration] = [
2468+
func_decl
2469+
for tool in req.config.tools or []
2470+
if isinstance(tool, types.Tool) and tool.function_declarations
2471+
for func_decl in tool.function_declarations
2472+
]
24652473
function_logs = (
24662474
[
24672475
_build_function_declaration_log(func_decl)

tests/unittests/models/test_litellm.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,105 @@ async def test_get_completion_inputs_inserts_missing_tool_results():
668668
assert tool_message["content"] == _MISSING_TOOL_RESULT_MESSAGE
669669

670670

671+
@pytest.mark.asyncio
672+
async def test_get_completion_inputs_serializes_native_only_tool():
673+
llm_request = LlmRequest(
674+
config=types.GenerateContentConfig(
675+
tools=[types.Tool(google_search=types.GoogleSearch())]
676+
)
677+
)
678+
679+
_, tools, _, _ = await _get_completion_inputs(
680+
llm_request, model="openai/gpt-4o"
681+
)
682+
683+
assert tools == [
684+
types.Tool(google_search=types.GoogleSearch()).model_dump(
685+
by_alias=True, exclude_none=True
686+
)
687+
]
688+
689+
690+
@pytest.mark.asyncio
691+
async def test_get_completion_inputs_mixed_native_and_function_tools():
692+
llm_request = LlmRequest(
693+
config=types.GenerateContentConfig(
694+
tools=[
695+
types.Tool(google_search=types.GoogleSearch()),
696+
types.Tool(
697+
function_declarations=[
698+
types.FunctionDeclaration(
699+
name="get_weather",
700+
description="Gets the weather.",
701+
parameters=types.Schema(
702+
type=types.Type.OBJECT,
703+
properties={
704+
"city": types.Schema(type=types.Type.STRING)
705+
},
706+
),
707+
)
708+
]
709+
),
710+
]
711+
)
712+
)
713+
714+
_, tools, _, _ = await _get_completion_inputs(
715+
llm_request, model="openai/gpt-4o"
716+
)
717+
718+
assert len(tools) == 2
719+
native_tools = [t for t in tools if "type" not in t]
720+
function_tools = [t for t in tools if t.get("type") == "function"]
721+
assert len(native_tools) == 1
722+
assert len(function_tools) == 1
723+
assert function_tools[0]["function"]["name"] == "get_weather"
724+
725+
726+
@pytest.mark.asyncio
727+
async def test_get_completion_inputs_collects_tools_beyond_index_zero():
728+
llm_request = LlmRequest(
729+
config=types.GenerateContentConfig(
730+
tools=[
731+
types.Tool(
732+
function_declarations=[
733+
types.FunctionDeclaration(
734+
name="first_tool", description="First tool."
735+
)
736+
]
737+
),
738+
types.Tool(
739+
function_declarations=[
740+
types.FunctionDeclaration(
741+
name="second_tool", description="Second tool."
742+
)
743+
]
744+
),
745+
]
746+
)
747+
)
748+
749+
_, tools, _, _ = await _get_completion_inputs(
750+
llm_request, model="openai/gpt-4o"
751+
)
752+
753+
assert [t["function"]["name"] for t in tools] == [
754+
"first_tool",
755+
"second_tool",
756+
]
757+
758+
759+
@pytest.mark.asyncio
760+
async def test_get_completion_inputs_no_tools_returns_none():
761+
llm_request = LlmRequest(config=types.GenerateContentConfig())
762+
763+
_, tools, _, _ = await _get_completion_inputs(
764+
llm_request, model="openai/gpt-4o"
765+
)
766+
767+
assert tools is None
768+
769+
671770
def test_schema_to_dict_filters_none_enum_values():
672771
# Use model_construct to bypass strict enum validation.
673772
top_level_schema = types.Schema.model_construct(

0 commit comments

Comments
 (0)