Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions src/google/adk/models/lite_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
99 changes: 99 additions & 0 deletions tests/unittests/models/test_litellm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading