Skip to content

AnthropicLlm ignores tool_config: function calling mode NONE or ANY never reaches Claude聽#7633

Description

@Somay-kousis

馃敶 Required Information

Describe the Bug:

AnthropicLlm (and Claude) always send tool_choice={"type": "auto"} when the agent has tools. The function calling mode set in generate_content_config.tool_config is dropped, so with a Claude model:

  • mode="NONE" does not stop tool calls.
  • mode="ANY" does not force a tool call.
  • mode="ANY", allowed_function_names=["x"] does not force tool x.

tool_config is how ADK sets tool choice (the answer on #773). Gemini and LiteLlm honor it (LiteLlm maps ANY to required and NONE to none). ADK's own model_consult force plugin relies on it too (_force_escalation_plugin.py:310-313 sets ANY with allowed_function_names=[tool]), so that forcing has no effect on Claude either.

Steps to Reproduce:

  1. Create an Agent with model=AnthropicLlm(...), a function tool, and generate_content_config=GenerateContentConfig(tool_config=ToolConfig(function_calling_config=FunctionCallingConfig(mode="NONE"))).
  2. Run it with InMemoryRunner.run_async.
  3. Look at the tool_choice sent to messages.create (the script below fakes the SDK call to record it).
  4. Repeat with mode="ANY" and with mode="ANY", allowed_function_names=["get_weather"].

Expected Behavior:

NONE None -> {'type': 'none'}
ANY None -> {'type': 'any'}
ANY ['get_weather'] -> {'type': 'tool', 'name': 'get_weather'}

Observed Behavior:

NONE None -> {'type': 'auto'}
ANY None -> {'type': 'auto'}
ANY ['get_weather'] -> {'type': 'auto'}

Environment Details:

  • ADK Library Version (pip show google-adk): 2.12.0, main at 662b017
  • Desktop OS: macOS 26.6.2
  • Python Version (python -V): Python 3.12.13

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: AnthropicLlm(model="claude-sonnet-4-5"), with AsyncMessages.create faked (anthropic 1.14.0), no real call

馃煛 Optional Information

Root cause:

src/google/adk/models/anthropic_llm.py:1255-1259 in AnthropicLlm.generate_content_async hardcodes the choice:

tool_choice = (
    anthropic_types.ToolChoiceAutoParam(type="auto")
    if llm_request.tools_dict
    else NOT_GIVEN
)

llm_request.config.tool_config is not read anywhere in the file. The streaming and non-streaming paths both use this value.

Minimal Reproduction Code:

import asyncio, os
from unittest import mock
from anthropic import types as at
from anthropic.resources.messages import AsyncMessages
from google.adk.agents import Agent
from google.adk.models.anthropic_llm import AnthropicLlm
from google.adk.runners import InMemoryRunner
from google.genai import types

os.environ.setdefault("ANTHROPIC_API_KEY", "fake")
sent = []

async def fake_create(self, **kwargs):
  sent.append(kwargs)
  return at.Message(
      id="m", content=[at.TextBlock(text="ok", type="text")],
      model=kwargs["model"], role="assistant", stop_reason="end_turn",
      stop_sequence=None, type="message",
      usage=at.Usage(input_tokens=1, output_tokens=1))

def get_weather(city: str) -> str:
  """Returns the weather for a city."""
  return f"Sunny in {city}"

async def run(fc):
  agent = Agent(
      name="weather", model=AnthropicLlm(model="claude-sonnet-4-5"),
      tools=[get_weather],
      generate_content_config=types.GenerateContentConfig(
          tool_config=types.ToolConfig(function_calling_config=fc)))
  runner = InMemoryRunner(agent=agent, app_name="r")
  s = await runner.session_service.create_session(app_name="r", user_id="u")
  sent.clear()
  async for _ in runner.run_async(
      user_id="u", session_id=s.id,
      new_message=types.Content(role="user", parts=[types.Part(text="Paris?")])):
    pass
  print(fc.mode.value, fc.allowed_function_names, "->", sent[0]["tool_choice"])

async def main():
  with mock.patch.object(AsyncMessages, "create", fake_create):
    await run(types.FunctionCallingConfig(mode="NONE"))
    await run(types.FunctionCallingConfig(mode="ANY"))
    await run(types.FunctionCallingConfig(
        mode="ANY", allowed_function_names=["get_weather"]))

asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions