diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 84dc0cc3c04..54585c786d0 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -27,7 +27,6 @@ import sys from typing import Any from typing import AsyncGenerator -from typing import cast from typing import Dict from typing import Generator from typing import Iterable @@ -2376,15 +2375,22 @@ async def _get_completion_inputs( # 2. Convert tool declarations tools: Optional[List[Dict[str, Any]]] = None - if ( - llm_request.config - and llm_request.config.tools - and llm_request.config.tools[0].function_declarations - ): - tools = [ - _function_declaration_to_tool_param(tool) - for tool in llm_request.config.tools[0].function_declarations - ] + if llm_request.config and llm_request.config.tools: + tools = [] + for tool in llm_request.config.tools: + if not isinstance(tool, types.Tool): + continue + if tool.function_declarations: + tools.extend( + _function_declaration_to_tool_param(func_decl) + for func_decl in tool.function_declarations + ) + else: + # Native/built-in tools (e.g. google_search) carry no + # function_declarations; serialize them as-is so they reach the + # provider or proxy instead of being silently dropped. + tools.append(tool.model_dump(by_alias=True, exclude_none=True)) + tools = tools or None # 3. Handle response format response_format: dict[str, Any] | None = None @@ -2458,10 +2464,12 @@ def _build_request_log(req: LlmRequest) -> str: The request log. """ - function_decls: list[types.FunctionDeclaration] = cast( - list[types.FunctionDeclaration], - req.config.tools[0].function_declarations if req.config.tools else [], - ) + function_decls: list[types.FunctionDeclaration] = [ + func_decl + for tool in req.config.tools or [] + if isinstance(tool, types.Tool) and tool.function_declarations + for func_decl in tool.function_declarations + ] function_logs = ( [ _build_function_declaration_log(func_decl) diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index a3dc43de1f0..86b1420c821 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -668,6 +668,105 @@ async def test_get_completion_inputs_inserts_missing_tool_results(): assert tool_message["content"] == _MISSING_TOOL_RESULT_MESSAGE +@pytest.mark.asyncio +async def test_get_completion_inputs_serializes_native_only_tool(): + llm_request = LlmRequest( + config=types.GenerateContentConfig( + tools=[types.Tool(google_search=types.GoogleSearch())] + ) + ) + + _, tools, _, _ = await _get_completion_inputs( + llm_request, model="openai/gpt-4o" + ) + + assert tools == [ + types.Tool(google_search=types.GoogleSearch()).model_dump( + by_alias=True, exclude_none=True + ) + ] + + +@pytest.mark.asyncio +async def test_get_completion_inputs_mixed_native_and_function_tools(): + llm_request = LlmRequest( + config=types.GenerateContentConfig( + tools=[ + types.Tool(google_search=types.GoogleSearch()), + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="get_weather", + description="Gets the weather.", + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "city": types.Schema(type=types.Type.STRING) + }, + ), + ) + ] + ), + ] + ) + ) + + _, tools, _, _ = await _get_completion_inputs( + llm_request, model="openai/gpt-4o" + ) + + assert len(tools) == 2 + native_tools = [t for t in tools if "type" not in t] + function_tools = [t for t in tools if t.get("type") == "function"] + assert len(native_tools) == 1 + assert len(function_tools) == 1 + assert function_tools[0]["function"]["name"] == "get_weather" + + +@pytest.mark.asyncio +async def test_get_completion_inputs_collects_tools_beyond_index_zero(): + llm_request = LlmRequest( + config=types.GenerateContentConfig( + tools=[ + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="first_tool", description="First tool." + ) + ] + ), + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="second_tool", description="Second tool." + ) + ] + ), + ] + ) + ) + + _, tools, _, _ = await _get_completion_inputs( + llm_request, model="openai/gpt-4o" + ) + + assert [t["function"]["name"] for t in tools] == [ + "first_tool", + "second_tool", + ] + + +@pytest.mark.asyncio +async def test_get_completion_inputs_no_tools_returns_none(): + llm_request = LlmRequest(config=types.GenerateContentConfig()) + + _, tools, _, _ = await _get_completion_inputs( + llm_request, model="openai/gpt-4o" + ) + + assert tools is None + + def test_schema_to_dict_filters_none_enum_values(): # Use model_construct to bypass strict enum validation. top_level_schema = types.Schema.model_construct(