OpenAI Agents SDK Duplicate Tool Names: Why the Later Tool Wins
The fixture uses Python 3.10.2 and openai-agents 0.19.2 and exercises only local tool registration, list resolution, and lookup-map construction. It requires no API key and makes no model call. The OpenAI API duplicate-name 400 is reported by upstream issue #4116 rather than independently tested here.
Who Should Read This
- ● Python developers combining multiple FunctionTools, plugins, or agent tools with OpenAI Agents SDK.
- ● Platform teams validating tool registries in CI, worker startup, and production release gates.
- ● Agent teams debugging duplicate function name 400 errors, wrong dispatch, or name_override collisions.
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. Upstream status changed after this fixture was published: issue #4116 is now closed, and PR #4145 was merged on August 4 with collision-policy work that lists #4116 as resolved. The experiment below still intentionally pins openai-agents==0.19.2, so it remains evidence for that version rather than a claim that every later package has identical behavior.
For an upgrade, use the exact version in your lockfile as the contract: run the duplicate-name regression before removing an application-level preflight. A closed issue is useful status information; a passing test on your deployed SDK is the stronger release signal.
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}"
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
lookuptool; - 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_namevalues; - multiple teams choose
execute,query, orfetchas 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.

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
Run it with:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python repro.py
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)
Passing the duplicate tools returns normally:
result = validate_function_tool_lookup_configuration([
lookup_customers,
lookup_orders,
])
assert result is None
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])
The fixture prints:
['lookup', 'lookup']
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"}
}
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,
])
Only one key remains:
('bare', 'lookup')
It points to the later tool:
selected = lookup_map[("bare", "lookup")]
assert selected is lookup_orders
assert selected is not lookup_customers
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
}
The later dictionary assignment replaces the earlier one.

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
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."
)
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,
)
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.
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}"
The resulting list is:
['lookup', 'lookup_orders']
Prefer business-specific names over numeric suffixes:
customer_lookup
order_lookup
invoice_lookup
knowledge_search
shipment_track
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
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)
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

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 the upstream status change
Issue #4116 is closed and PR #4145 merged on August 4, but the safe release gate is still behavioral. This article’s fixture proves the old 0.19.2 failure mode; it does not substitute for testing the package version you are about to deploy. A collision-safe release should reject or otherwise resolve the configuration unambiguously during tool resolution, with an actionable error when the same bare public name is invalid, for example:
Ambiguous function tool configuration:
the tool name `lookup` is used by multiple tools.
Pass a unique name_override= or namespace.
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,
])
Also verify that:
Agent.get_all_tools()cannot expose duplicate sendable names;- dynamic and static tool collisions are both detected;
- namespace rules match the documented behavior;
- the error identifies the conflicting name and remediation;
- 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
For the 0.19.2 behavior reproduced here—and for any later release that has not yet passed your own collision regression—production systems should:
- validate unique
FunctionTool.namevalues after final registry assembly; - use explicit business-specific names or
name_override; - organize larger tool sets with namespaces;
- run the check in factory tests, plugin registration, and release gates;
- keep a version-pinned regression test during SDK upgrades.
This configuration should fail at local startup, not through a provider 400 or an incorrect external side effect.
References
- OpenAI Agents SDK issue #4116: Reject duplicate function tool names
- OpenAI Agents SDK: Tools
- OpenAI Agents SDK: Agents
Continue from one agent pattern to the complete production system
The AI Agent hub organizes architecture, memory, tool use, evaluation, security, deployment and multi-agent coordination into a single learning path.
Next Reading
View Hub →
OpenAI Agents SDK Tool Approval Resume: RunState Across Processes and the v0.19.3 Streaming Fix
Compare OpenAI Agents SDK 0.18.3 and 0.19.3: reproduce the streamed-resume approved tool-output loss, verify the fix, and test cross-process RunState recovery.
Implementing AI Agent Memory Systems: A 3-Layer Architecture and Practical Code to Solve Agent 'Amnesia'
Practical guide to implementing AI Agent memory systems. Compares the performance of vector databases versus graph databases for long-term memory storage.
AutoGen Hands-On Tutorial: Multi-AI Agent Conversational Collaboration, Tool Invocation, and Production Deployment Boundaries
AutoGen Hands-On Tutorial: Systematically deconstruct AutoGen's practical usage and production deployment boundaries in multi-agent conversational collaboration, covering AgentChat
LangChain Tutorial: Building an AI Agent with Tool Calling
A LangChain tutorial for building an AI agent with Pydantic tool schemas, AgentExecutor control flow, persistent memory, error handling, and production boundaries.
Xiaobai
Full-Stack AI Engineer
Xiaobai, a full-stack AI engineer building production Agent systems, product tools and independent software assets.
About Xiaobai & XBSTACK →Liked this article?
Join the newsletter
Every issue condenses production AI engineering changes, real failures, reproducible experiments, useful tools and new XBSTACK assets. No generic news digest and no filler.
DISCUSSION
Questions, verification and corrections
Sign in to comment. Every new comment is reviewed before publication; while pending, it is visible only to you and the administrator.