Skip to content

fix: AgentTool with input_schema + LiteLLM/Claude skips inner agent tool calls #5926

@ecanlar

Description

@ecanlar

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.

Metadata

Metadata

Labels

models[Component] Issues related to model supportrequest clarification[Status] The maintainer need clarification or more information from the author

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions