Bug description
When an AgentTool is constructed with input_schema=<PydanticModel> and the inner agent uses a Claude model via LiteLLM (LiteLlm("anthropic/claude-*")), the inner agent responds directly without entering the ReAct tool-calling loop. Removing input_schema restores the expected behaviour.
Reproduction
from pydantic import BaseModel
from google.adk.agents import LlmAgent
from google.adk.tools import FunctionTool
from google.adk.tools.agent_tool import AgentTool
from google.adk.models.lite_llm import LiteLlm
class MyInput(BaseModel):
repository_url: str
base_branch: str
def my_tool(path: str) -> str:
"""A tool the inner agent should call."""
return f"content of {path}"
inner_agent = LlmAgent(
name="inner",
model=LiteLlm("anthropic/claude-sonnet-4-6"),
tools=[FunctionTool(func=my_tool)],
instruction="Always call my_tool with the repository_url before answering.",
)
# With input_schema: inner agent responds directly, never calls my_tool
tool_with_schema = AgentTool(agent=inner_agent, input_schema=MyInput)
# Without input_schema: inner agent calls my_tool as expected
tool_without_schema = AgentTool(agent=inner_agent)
Expected: inner agent calls my_tool before producing a response.
Actual: inner agent responds with text immediately, never invoking my_tool.
Root cause analysis
Two concurring causes identified in ADK 1.28.0:
Cause 1 — agent_tool.py (lines ~205–214)
When input_schema is present, run_async sends the inner agent a raw serialized JSON blob as its first message:
# Current code
text=input_value.model_dump_json(exclude_none=True)
# e.g. '{"repository_url": "https://...", "base_branch": "main"}'
Claude interprets a structured JSON as a completed specification and produces a direct answer rather than initiating tool exploration. Without input_schema, the message is args['request']—natural language—which correctly triggers the ReAct loop.
Proposed fix for agent_tool.py:
if input_schema:
input_value = input_schema.model_validate(args)
json_payload = input_value.model_dump_json(exclude_none=True)
content = types.Content(
role='user',
parts=[types.Part.from_text(
text=(
'Process the following structured request. Use your available '
'tools as needed to gather information or perform actions '
'before producing the final response.\n\n'
f'Request:\n{json_payload}'
)
)],
)
Cause 2 — lite_llm.py (addresses #773)
_get_completion_inputs and generate_content_async never extract tool_config.function_calling_config.mode from LlmRequest nor pass tool_choice to LiteLLM's acompletion(). Without tool_choice="required", Claude is not forced to call tools even when the caller intends it.
Proposed fix for lite_llm.py (_get_completion_inputs, before return):
tool_choice = None
if (
llm_request.config
and llm_request.config.tool_config
and llm_request.config.tool_config.function_calling_config
):
from google.genai import types as genai_types
mode = llm_request.config.tool_config.function_calling_config.mode
if mode == genai_types.FunctionCallingConfigMode.ANY:
tool_choice = 'required'
elif mode == genai_types.FunctionCallingConfigMode.NONE:
tool_choice = 'none'
# AUTO → leave as None (provider default)
And in generate_content_async (around line 2230):
if tool_choice is not None:
completion_args['tool_choice'] = tool_choice
Note: anthropic_llm.py hardcodes tool_choice="auto" (line ~489) — same propagation logic should be applied there for consistency.
Environment
- ADK version: 1.28.0
- Model:
claude-sonnet-4-6 via LiteLLM (LiteLlm("anthropic/claude-sonnet-4-6"))
- Python: 3.12
Related issues
Notes
The combination AgentTool + input_schema + native Gemini API may work correctly because Gemini's training makes it more likely to call tools regardless of message format. The bug manifests specifically with Claude-family models via LiteLLM, where the raw JSON prompt is a reliable trigger for skipping the ReAct loop.
Bug description
When an
AgentToolis constructed withinput_schema=<PydanticModel>and the inner agent uses a Claude model via LiteLLM (LiteLlm("anthropic/claude-*")), the inner agent responds directly without entering the ReAct tool-calling loop. Removinginput_schemarestores the expected behaviour.Reproduction
Expected: inner agent calls
my_toolbefore producing a response.Actual: inner agent responds with text immediately, never invoking
my_tool.Root cause analysis
Two concurring causes identified in ADK 1.28.0:
Cause 1 —
agent_tool.py(lines ~205–214)When
input_schemais present,run_asyncsends the inner agent a raw serialized JSON blob as its first message:Claude interprets a structured JSON as a completed specification and produces a direct answer rather than initiating tool exploration. Without
input_schema, the message isargs['request']—natural language—which correctly triggers the ReAct loop.Proposed fix for
agent_tool.py:Cause 2 —
lite_llm.py(addresses #773)_get_completion_inputsandgenerate_content_asyncnever extracttool_config.function_calling_config.modefromLlmRequestnor passtool_choiceto LiteLLM'sacompletion(). Withouttool_choice="required", Claude is not forced to call tools even when the caller intends it.Proposed fix for
lite_llm.py(_get_completion_inputs, beforereturn):And in
generate_content_async(around line 2230):Note:
anthropic_llm.pyhardcodestool_choice="auto"(line ~489) — same propagation logic should be applied there for consistency.Environment
claude-sonnet-4-6via LiteLLM (LiteLlm("anthropic/claude-sonnet-4-6"))Related issues
input_schemadoes not work (closed in v1.20.0) — fix there correctedFunctionDeclarationdescription but not the tool-calling behaviour described hereNotes
The combination
AgentTool + input_schema + native Gemini APImay work correctly because Gemini's training makes it more likely to call tools regardless of message format. The bug manifests specifically with Claude-family models via LiteLLM, where the raw JSON prompt is a reliable trigger for skipping the ReAct loop.