Skip to content

Reject duplicate function tool names #4116

Description

@rajarshidattapy

Describe the bug

When an Agent is given two FunctionTools that share the same name, the SDK neither rejects nor deduplicates them. Both are advertised to the model in the tools array, and at dispatch time the second one silently shadows the first, which becomes permanently unreachable.

The SDK clearly intends to catch this. agents._tool_identity.validate_function_tool_lookup_configuration() is called from build_function_tool_lookup_map() and exists specifically to "Reject function-tool combinations that are ambiguous on the Responses wire". It detects the collision correctly, and then explicitly declines to act on it:

# src/agents/_tool_identity.py
prior_owner = qualified_name_owners.get(qualified_name)
if prior_owner is None:
    qualified_name_owners[qualified_name] = tool
    continue

prior_namespace = get_explicit_function_tool_namespace(prior_owner)
if explicit_namespace is None and prior_namespace is None:
    continue          # <-- collision detected, then ignored

raise UserError(
    "Ambiguous function tool configuration: the qualified name "
    f"`{qualified_name}` is used by multiple tools. ..."
)

The continue on the "neither tool has an explicit namespace" branch is exactly the common case: two plain @function_tools. So the guard fires for namespaced tools but is a no-op for the configuration users are most likely to hit.

Everything downstream then behaves as if this were legal:

  • build_function_tool_lookup_map() is documented as using "last-wins precedence", so the map ends up pointing at whichever duplicate appears later in agent.tools.
  • Agent.get_all_tools() returns both tools unchanged, so the model receives tools: [..., {"name": "lookup"}, {"name": "lookup"}, ...].

The OpenAI API rejects duplicate function names in a single request, so in production this surfaces as an opaque provider-side 400 rather than a clear SDK error. On providers that tolerate duplicates, it is worse: the run succeeds while silently executing the wrong implementation.

This is easy to hit unintentionally — for example when two tool modules are combined, when a helper is exported under an explicit name_override, or when Agent.clone()/list concatenation appends a tool that is already present.

The SDK already implements exactly this validation elsewhere, which makes the gap look like an oversight rather than a deliberate policy:

  • MCP tools: MCPUtil.get_all_function_tools() raises UserError("Duplicate tool names found across MCP servers: ...").
  • Codex tools: Agent.get_all_tools() calls _validate_codex_tool_name_collisions(), which raises UserError("Duplicate Codex tool names found: ... Provide a unique codex_tool(name=...) per tool instance.").

Plain function tools are the only category with no equivalent check. (Note this is also distinct from #3561, which added duplicate detection within an individual MCP server.)

Debug information

  • Agents SDK version: 0.19.2 (reproduced on main at commit fc084ae2)
  • Related library versions (optional, e.g. any-llm, litellm, or pydantic): n/a
  • Python version: 3.13.6
  • Operating system: Windows 11 (not platform specific)
  • Model and model provider: Reproduced offline with a stub model so the wrong-tool dispatch is observable without a network call. Against OpenAI (Responses or Chat Completions) the same agent configuration fails with a duplicate-function-name 400.
  • Does the issue reproduce with the latest Agents SDK release? Yes, on main at fc084ae2.
  • Does the issue occur consistently or intermittently? Consistently and deterministically.

No exception is raised by the SDK — that is the bug. Output of the repro below:

duplicate tool names accepted; final: done
tools sent to model: ['lookup', 'lookup']
which ran -> ['b']

which ran -> ['b'] shows the second registered tool served a call that the first tool was equally eligible for. The first tool can never be invoked.

Repro steps

This uses the repository's tests/fake_model.py stub so the dispatch outcome is visible without an API key. The same agent configuration against a real OpenAI model fails with a 400 instead.

import asyncio
import json
import sys

sys.path.insert(0, "tests")  # run from the repo root

from agents import Agent, Runner, function_tool
from fake_model import FakeModel
from test_responses import get_function_tool_call, get_text_message


@function_tool(name_override="lookup")
def lookup_customers(x: str) -> str:
    """Look up customers."""
    return "a"


@function_tool(name_override="lookup")
def lookup_orders(x: str) -> str:
    """Look up orders."""
    return "b"


async def main() -> None:
    model = FakeModel()
    agent = Agent(name="A", model=model, tools=[lookup_customers, lookup_orders])
    model.add_multiple_turn_outputs(
        [
            [get_function_tool_call("lookup", json.dumps({"x": "1"}), call_id="z1")],
            [get_text_message("done")],
        ]
    )

    result = await Runner.run(agent, "hi")

    # No UserError was raised anywhere above.
    print("tools sent to model:", [t.name for t in model.first_turn_args["tools"]])
    # ['lookup', 'lookup']  -> the API rejects this payload

    print(
        "which ran ->",
        [i.raw_item["output"] for i in result.new_items if i.type == "tool_call_output_item"],
    )
    # ['b']  -> lookup_orders silently shadowed lookup_customers


asyncio.run(main())

Equivalent minimal form without the test stub, showing the validator itself declines to raise:

from agents import function_tool
from agents._tool_identity import validate_function_tool_lookup_configuration


@function_tool(name_override="lookup")
def a(x: str) -> str:
    """A."""
    return "a"


@function_tool(name_override="lookup")
def b(x: str) -> str:
    """B."""
    return "b"


validate_function_tool_lookup_configuration([a, b])  # returns None; expected UserError

Expected behavior

Two function tools that resolve to the same public name on a single agent should raise a UserError at tool-resolution time, consistent with how the SDK already treats duplicate MCP tool names and duplicate Codex tool names.

Concretely, in validate_function_tool_lookup_configuration(), the explicit_namespace is None and prior_namespace is None branch should raise rather than continue. The error should name the colliding tool name and suggest name_override= as the fix, mirroring the existing messages:

Ambiguous function tool configuration: the tool name `lookup` is used by multiple tools.
Pass a unique `name_override=` to one of them to avoid ambiguous dispatch.

Failing that, the collision should at minimum produce a warning and the duplicate should be dropped from the payload sent to the model, so the SDK never emits a tools array the API is guaranteed to reject.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions