Repository navigation
Description
Describe the Bug:
ApigeeLlm (Chat Completions) ignores FunctionCallingConfig.allowed_function_names and the VALIDATED mode. _map_tools in src/google/adk/models/apigee_llm.py (lines 765-789 at b8836bdb, 815-822 on current main) maps only mode: ANY -> "required", NONE -> "none", AUTO -> "auto". So "call lookup_order" becomes "call any tool", and "only these tools" becomes no restriction. No warning is logged. We found this while testing how SDKs carry tool-call constraints to the provider.
At the same commit, AnthropicLlm ignored tool_config entirely and always sent tool_choice: {"type": "auto"} (anthropic_llm.py:1068-1072). This looks fixed on main by _build_tool_choice. One case may remain: when llm_request.tools_dict is empty (a direct generate_content_async call with tools only in config.tools), tool_choice is still NOT_GIVEN even if tool_config is set. We have not rerun that on main.
Steps to Reproduce:
- Start a local HTTP server that logs request bodies.
- Create an
ApigeeLlmwithproxy_urlpointing at it andapi_type="chat_completions". - Run an
LlmAgentwith two tools and the config below. - Look at
tool_choicein the captured request.
Expected Behavior:
mode=ANY, allowed_function_names=["lookup_order"] -> "tool_choice": {"type": "function", "function": {"name": "lookup_order"}}. mode=VALIDATED (or AUTO) with allowed_function_names -> an allowed_tools choice, or the other tools removed from the request, or an error saying the restriction cannot be sent.
Observed Behavior:
- ANY +
["lookup_order"]:"tool_choice": "required", both tools declared. In our test the server answered withissue_refundand the agent ran it. - VALIDATED +
["lookup_order"]: notool_choice, both tools declared. The agent ranissue_refund.
Environment Details:
- ADK Library Version: google-adk 2.9.2 (commit
b8836bdb). Apigee code unchanged onmainat9687bae1. - Desktop OS: Linux (Ubuntu 24.04)
- Python Version: 3.12
Model Information:
- Are you using LiteLLM: No
- Which model is being used:
apigee/openai/gpt-4o(Chat Completions) andclaude-sonnet-4-5throughAnthropicLlm
Minimal Reproduction Code:
from google.adk.agents import LlmAgent
from google.adk.models.apigee_llm import ApigeeLlm
from google.adk.runners import InMemoryRunner
from google.genai import types
def lookup_order(order_id: str) -> str:
return "delivered"
def issue_refund(order_id: str, amount: int) -> str:
return "refunded"
cfg = types.GenerateContentConfig(tool_config=types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(
mode=types.FunctionCallingConfigMode.ANY, allowed_function_names=["lookup_order"])))
agent = LlmAgent(name="app", instruction="Use the tools.",
model=ApigeeLlm(model="apigee/openai/gpt-4o", proxy_url="http://127.0.0.1:8080/v1",
api_type="chat_completions"),
tools=[lookup_order, issue_refund], generate_content_config=cfg)
runner = InMemoryRunner(agent=agent)
# run one turn with runner.run_async(...) and inspect the captured requestHow often has this issue occurred?: Always (deterministic, request construction)
Additional Context:
Impact: an application that forces or limits tool calls through FunctionCallingConfig expects the model to be held to that. On these backends the restriction is not sent, so any declared tool can be called.
Suggested fix: map allowed_function_names with one name to a named function choice, map a subset to allowed_tools (or filter tools), and raise or warn for combinations that cannot be expressed.
Activity
Binary-Refinery commentedon Nov 12, 2026
I can extend the local HTTP reproduction to cover two allowed tools and one excluded tool, then check VALIDATED separately. I’ll report the outbound payload and leave the existing single-tool fix in #8101 scoped as it is.
surajksharma07 commentedon Nov 12, 2026
@Android-Login-Analysis, easy to follow. Reproduced on 2.14.0 and current main: ANY + ["lookup_order"] goes out as "required", VALIDATED sends no tool_choice, and issue_refund runs in both. On the Claude side the main fix shipped in 2.14.0, but the empty tools_dict case you mentioned is still there.
@prassu017 #8101 looks good for the single-name case. For the rest, this worked locally: for ANY/VALIDATED, trim payload["tools"] down to allowed_function_names, map VALIDATED to "auto", and raise if an allowed name isn’t a declared tool. Could you try that on your side, including two allowed names plus an excluded one? Please test it thoroughly before folding it into the PR. Maintainers may still prefer allowed_tools over filtering.
@Binary-Refinery your two-allowed/one-excluded payload check would be a good way to validate this. Could you share the outbound request bodies once you have them?