DEV Community

Cover image for OpenAI Agents SDK Duplicate Tool Names: Why the Later Tool Wins
xn
xn

Posted on • Originally published at xbstack.com

OpenAI Agents SDK Duplicate Tool Names: Why the Later Tool Wins

A production-focused update based on a real project: An offline reproduction on openai-agents 0.19.2 shows that duplicate FunctionTool names pass SDK validation, remain in Agent.g…

OpenAI Agents SDK Duplicate Tool Names: Why the Later Tool Wins

Here is the direct result: with openai-agents==0.19.2, two plain FunctionTool objects can expose the same public name without the SDK rejecting the Agent configuration. In the offline fixture, two different Python functions both use name_override="lookup". The SDK validation function returns None, Agent.get_all_tools() still returns two lookup tools, and the internal dispatch lookup map keeps only the later one. With OpenAI APIs, the request can fail with a duplicate-function-name 400. With a compatible provider that accepts duplicate names, the run may continue while the wrong local implementation executes.

The issue was filed in the official OpenAI Agents SDK repository on August 2, 2026 as issue #4116. As of August 3, it remained open with no linked pull request.

This page answers one narrow search problem:

Why can duplicate FunctionTool names enter an OpenAI Agents SDK Agent, why does local dispatch become last-wins, and how can a production service fail before sending a model request?

It is separate from the existing RunState approval resume guide. That article covers interruptions, approval, serialization, and cross-process recovery. This one covers tool-registry identity before the model call starts.

The smallest collision

The Python function names are different, but both public tool names are lookup:

from agents import function_tool


@function_tool(name_override="lookup")
def lookup_customers(query: str) -> str:
    """Look up customers."""
    return f"customer:{query}"


@function_tool(name_override="lookup")
def lookup_orders(query: str) -> str:
    """Look up orders."""
    return f"order:{query}"
Enter fullscreen mode Exit fullscreen mode

The official tools documentation says @function_tool normally uses the Python function name and allows an explicit name_override. The identity that must be unique is the resulting FunctionTool.name, not the Python identifier.

This collision can emerge when:

  • CRM and order modules both export a generic lookup tool;
  • two plugins expose search;
  • an Agent.clone() flow appends the original tools again;
  • tenant or feature-flag logic adds tools dynamically;
  • sub-agents are converted to tools with repeated tool_name values;
  • multiple teams choose execute, query, or fetch as overrides;
  • an old and new implementation are registered during a migration.

Each module may be valid in isolation. The conflict appears only after the final registry is assembled.

Two OpenAI Agents SDK FunctionTools expose the same lookup name, both enter the Agent tool list, and local dispatch keeps only the later lookup_orders implementation

Offline test environment

The fixture avoids model behavior, API keys, network calls, and provider differences:

Component Value
Python 3.10.2
OpenAI Agents SDK 0.19.2
API key Not required
Model call None
Verified paths Tool validation, Agent tool list, dispatch lookup map

Files:

experiments/openai-agents-duplicate-tool-names-repro/
├── repro.py
├── requirements.txt
├── results/verification.json
├── RESEARCH.md
└── README.md
Enter fullscreen mode Exit fullscreen mode

Run it with:

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python repro.py
Enter fullscreen mode Exit fullscreen mode

Layer one: the SDK validator does not reject the conflict

The SDK contains a function with an explicit purpose:

validate_function_tool_lookup_configuration(tools)
Enter fullscreen mode Exit fullscreen mode

Passing the duplicate tools returns normally:

result = validate_function_tool_lookup_configuration([
    lookup_customers,
    lookup_orders,
])

assert result is None
Enter fullscreen mode Exit fullscreen mode

There is no UserError, warning, or deduplication.

Issue #4116 identifies the relevant branch: the validator detects an existing owner for the same qualified name, but when neither plain tool has an explicit namespace, it executes continue. The common collision is recognized and then ignored.

The issue also notes inconsistent handling across tool categories: duplicate names from MCP servers and Codex tools already have explicit checks, while plain FunctionTools do not receive the equivalent gate.

Layer two: both duplicate tools remain visible to the model

After constructing the Agent:

from agents import Agent, RunContextWrapper

agent = Agent(
    name="Support",
    tools=[lookup_customers, lookup_orders],
)

tools = await agent.get_all_tools(
    RunContextWrapper(context=None),
)

print([tool.name for tool in tools])
Enter fullscreen mode Exit fullscreen mode

The fixture prints:

['lookup', 'lookup']
Enter fullscreen mode Exit fullscreen mode

The SDK does not rename, select, or remove one of them at the Agent layer. The provider request can therefore contain two function definitions with the same public name.

The upstream issue reports that OpenAI Responses and Chat Completions APIs reject this payload, producing a provider-side 400 even though the actual configuration error is local.

Layer three: local dispatch becomes last-wins

If a compatible provider accepts duplicate names and returns a tool call such as:

{
  "name": "lookup",
  "arguments": {"query": "A-100"}
}
Enter fullscreen mode Exit fullscreen mode

then the SDK still needs to select a Python implementation.

The fixture builds the internal lookup map:

from agents._tool_identity import build_function_tool_lookup_map

lookup_map = build_function_tool_lookup_map([
    lookup_customers,
    lookup_orders,
])
Enter fullscreen mode Exit fullscreen mode

Only one key remains:

('bare', 'lookup')
Enter fullscreen mode Exit fullscreen mode

It points to the later tool:

selected = lookup_map[("bare", "lookup")]

assert selected is lookup_orders
assert selected is not lookup_customers
Enter fullscreen mode Exit fullscreen mode

Verified output:

{
  "sdk_validator_returned_none": true,
  "advertised_tool_names": ["lookup", "lookup"],
  "dispatch_lookup_keys": [["bare", "lookup"]],
  "dispatch_selected_python_function": "lookup_orders",
  "first_tool_reachable_by_bare_name": false,
  "second_tool_reachable_by_bare_name": true
}
Enter fullscreen mode Exit fullscreen mode

The later dictionary assignment replaces the earlier one.

Offline verification on openai-agents 0.19.2: the SDK validator does not fail, the Agent exposes two lookup tools, and local dispatch selects lookup_orders

Why silent last-wins is more dangerous than a 400

A provider 400 stops the request. It harms availability, but it does not execute the wrong operation. A tolerant provider can be worse: the model returns lookup, and the local SDK cannot infer whether it meant customer lookup or order lookup. It executes whichever implementation survived the map construction.

Imagine that the two underlying implementations are:

lookup_customer_account
lookup_refund_order
Enter fullscreen mode Exit fullscreen mode

If both are exposed as lookup, one tool may read customer data while the other initiates a refund, delete, message send, or database write. The identity collision becomes a side-effect risk.

The failure surface includes:

  • two indistinguishable schemas sent to the model;
  • provider request rejection;
  • incorrect local implementation selection;
  • audit logs that show only the ambiguous public name;
  • retries that repeat the deterministic configuration problem;
  • different failure behavior after switching providers.

Temporary fix: validate uniqueness before model dispatch

Until the SDK rejects duplicate plain FunctionTools, applications can fail after assembling the final registry and before constructing the production request:

from collections import Counter
from collections.abc import Iterable

from agents import FunctionTool
from agents.exceptions import UserError


def find_duplicate_function_tool_names(
    tools: Iterable[object],
) -> list[str]:
    names = [
        tool.name
        for tool in tools
        if isinstance(tool, FunctionTool)
    ]

    return sorted(
        name
        for name, count in Counter(names).items()
        if count > 1
    )


def require_unique_function_tool_names(
    tools: Iterable[object],
) -> None:
    duplicates = find_duplicate_function_tool_names(tools)
    if duplicates:
        quoted = ", ".join(repr(name) for name in duplicates)
        raise UserError(
            "Duplicate FunctionTool names are not allowed: "
            f"{quoted}. Use a unique Python function name, "
            "name_override=, or a tool namespace."
        )
Enter fullscreen mode Exit fullscreen mode

Apply it to the final set:

tools = load_static_tools()
tools += load_plugin_tools()
tools += await load_tenant_tools(tenant_id)

require_unique_function_tool_names(tools)

agent = Agent(
    name="Support",
    tools=tools,
)
Enter fullscreen mode Exit fullscreen mode

The duplicate fixture now fails locally with an actionable message instead of waiting for a provider response:

Duplicate FunctionTool names are not allowed: 'lookup'.
Use a unique Python function name, name_override=, or a tool namespace.
Enter fullscreen mode Exit fullscreen mode

Fix the public names

Give the second tool a distinct identity:

@function_tool(name_override="lookup_orders")
def lookup_orders(query: str) -> str:
    """Look up orders."""
    return f"order:{query}"
Enter fullscreen mode Exit fullscreen mode

The resulting list is:

['lookup', 'lookup_orders']
Enter fullscreen mode Exit fullscreen mode

Prefer business-specific names over numeric suffixes:

customer_lookup
order_lookup
invoice_lookup
knowledge_search
shipment_track
Enter fullscreen mode Exit fullscreen mode

A good name helps the model distinguish capabilities as well as satisfying the uniqueness constraint.

Use namespaces for larger registries

The official tools documentation recommends namespaces where possible, especially when many related tools exist. A registry can expose clearer identities such as:

crm.lookup_customer
orders.lookup_order
billing.lookup_invoice
Enter fullscreen mode Exit fullscreen mode

Namespaces reduce collisions on generic verbs such as lookup, search, and create, and give the model a better high-level surface.

They do not replace testing. CI should validate the actual assembled callable identities rather than only checking source-level function names.

Where the gate should run

Agent factory unit tests

def test_support_agent_tool_names_are_unique():
    tools = build_support_tools()
    require_unique_function_tool_names(tools)
Enter fullscreen mode Exit fullscreen mode

Plugin registration

Validate after all plugins load. Per-plugin uniqueness cannot detect collisions between plugins.

Multi-tenant configuration

Different tenants can enable different combinations. Precompute valid combinations or validate and cache each final set at worker startup or request entry.

Clone and dynamic append paths

Cloning, list concatenation, feature flags, and A/B tests are common duplication sources. Validate the final list rather than the initial constant.

Release gates

CI can enumerate production factories:

for agent_name, tools in all_production_toolsets():
    try:
        require_unique_function_tool_names(tools)
    except UserError as exc:
        raise AssertionError(f"{agent_name}: {exc}") from exc
Enter fullscreen mode Exit fullscreen mode

Mitigation and release gate for duplicate OpenAI Agents SDK tool names: assemble the final tool set, run a unique-name preflight, resolve collisions, and enforce factory and CI checks

Name uniqueness is only the first registry check

A production tool registry should also validate:

Check Failure risk
Unique FunctionTool.name Provider 400 or wrong dispatch
Stable tool schema Cache invalidation and argument drift
Distinct descriptions Unstable model selection
Approval or guardrails for high-risk tools Unauthorized side effects
Stable tool IDs in audit logs Inability to identify implementation
Reproducible dynamic enablement Different workers expose different registries

For authorization and policy enforcement, continue with the AI Agent Tool Authorization Policy Gate. This page remains limited to naming identity.

Three approaches to avoid

Waiting for the provider 400

A deterministic local error is delayed until after network work, increasing latency, retry noise, and debugging cost. A provider change can convert the visible 400 into silent wrong dispatch.

Treating list order as configuration

Import order, plugin discovery, and configuration merging can change ordering. Last-wins is not an explicit or auditable routing policy.

Checking only __name__

Different Python functions can share the same public name through name_override. Validate FunctionTool.name.

Regression test after an upstream fix

As of August 3, 2026, issue #4116 had no linked pull request. A reasonable upstream fix should reject the configuration during tool resolution with an error that names the conflict and suggests a unique override or namespace, for example:

Ambiguous function tool configuration:
the tool name `lookup` is used by multiple tools.
Pass a unique name_override= or namespace.
Enter fullscreen mode Exit fullscreen mode

After upgrading, test the SDK behavior directly:

import pytest
from agents.exceptions import UserError


def test_sdk_rejects_duplicate_bare_function_tools():
    with pytest.raises(UserError):
        validate_function_tool_lookup_configuration([
            lookup_customers,
            lookup_orders,
        ])
Enter fullscreen mode Exit fullscreen mode

Also verify that:

  1. Agent.get_all_tools() cannot expose duplicate sendable names;
  2. dynamic and static tool collisions are both detected;
  3. namespace rules match the documented behavior;
  4. the error identifies the conflicting name and remediation;
  5. pre-request behavior is consistent across providers.

Relationship to the RunState article

The OpenAI Agents SDK RunState guide covers:

  • tool-approval interruptions;
  • RunState serialization;
  • cross-process resume;
  • redelivery and business idempotency;
  • context filtering and version governance.

This problem happens earlier. The tool registry is already ambiguous before the Agent sends a model request. A durable approval pipeline cannot make an ambiguous tool identity safe.

Conclusion

Duplicate FunctionTool names in OpenAI Agents SDK 0.19.2 have two failure modes:

  • strict providers reject the duplicate function definitions;
  • tolerant providers allow the request, while local dispatch keeps the later implementation.

The offline XBSTACK fixture verifies:

SDK validator       -> no error
Agent tools          -> ['lookup', 'lookup']
Local dispatch map   -> later lookup_orders only
First tool reachable -> False
Enter fullscreen mode Exit fullscreen mode

Until an upstream release rejects the conflict, production systems should:

  1. validate unique FunctionTool.name values after final registry assembly;
  2. use explicit business-specific names or name_override;
  3. organize larger tool sets with namespaces;
  4. run the check in factory tests, plugin registration, and release gates;
  5. keep a regression test during SDK upgrades.

This configuration should fail at local startup, not through a provider 400 or an incorrect external side effect.

References


Canonical article on XBSTACK:https://www.xbstack.com/en/ai/openai-agents-sdk-duplicate-tool-names/?utm_source=devto&utm_medium=community&utm_campaign=article_distribution&utm_content=openai-agents-sdk-duplicate-tool-names&ref=devto

标签:#AI #SoftwareEngineering #DeveloperTools #OpenAI Agents SDK #FunctionTool

Top comments (0)