Xiaobai

Xiaobai

Developer · Builder

Building AI engineering systems, developer tools and long-term digital assets at XBSTACK.

About Xiaobai & XBSTACK →
Two OpenAI Agents SDK FunctionTools named lookup are registered while local dispatch keeps only the later tool

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

OpenAI Agents SDK duplicate tool names can trigger a provider 400 or last-wins local dispatch. Reproduced on 0.19.2 and still present in 0.22.0; add a preflight uniqueness gate.

Published · 2026-08-039 min readXBSTACK
#OpenAI Agents SDK#FunctionTool#Tool Calling#Python#AI Agent#name_override#Tool Namespace#Production Engineering

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

Here is the direct result: duplicate plain FunctionTool names still reproduce on the current openai-agents==0.22.0 release. XBSTACK first verified the behavior on 0.19.2, then re-ran the same offline collision on August 28, 2026 with 0.22.0. In both versions, two different Python functions can share name_override="lookup", the SDK validation function returns normally, Agent.get_all_tools() exposes two lookup tools, and the internal dispatch lookup map keeps only the later one. With OpenAI APIs, upstream issue #4116 reports that the request can fail with a duplicate-function-name 400. With a compatible provider that accepts duplicates, 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. The issue is now closed and PR #4145 was merged on August 4. That repository status is not sufficient evidence that the released behavior is gone: the new XBSTACK 0.22.0 regression still reproduces the same plain-name ambiguity locally.

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. As of this August 28 test, 0.22.0 does not pass that gate.

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 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 and version matrix

The fixtures avoid model behavior, API keys, network calls, and provider differences:

ComponentValue
Python3.10.2
OpenAI Agents SDK0.19.2 baseline; 0.22.0 current-release regression
API keyNot required
Model callNone
Verified pathsTool validation, Agent tool list, dispatch lookup map

The current regression matrix is intentionally small and evidence-focused:

VersionValidatorAgent.get_all_tools()Bare lookup map
0.19.2accepts duplicate['lookup', 'lookup']later lookup_orders wins
0.22.0accepts duplicate['lookup', 'lookup']later lookup_orders wins

The second row was generated on August 28 with a fresh virtual environment and openai-agents==0.22.0. No API key or model request was used.

Files:

experiments/openai-agents-duplicate-tool-names-repro/
├── repro.py
├── verify_current_release.py
├── version-matrix.md
├── requirements.txt
├── results/verification.json
├── results/current-release.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.

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

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.

Current 0.22.0 regression: the closed issue still reproduces locally.

The new current-release test uses the same two public names but does not assume the old outcome. It catches UserError independently at three boundaries: explicit validation, Agent tool resolution, and lookup-map construction. On 0.22.0, all three observations still match the original failure mode:

Checkopenai-agents==0.22.0 result
Python3.10.2
Validator rejects duplicatesNo
Advertised tool names['lookup', 'lookup']
Bare lookup-map winnerlookup_orders
API/model call requiredNo

This changes the production decision. The application-side uniqueness gate is no longer merely a workaround for one old 0.19.2 fixture. It is still a defensible release guard for teams on 0.22.0 unless their own assembled tool registry is guaranteed unique by another deterministic layer.

The evidence boundary remains important: the fixture verifies local SDK behavior only. It does not independently call the OpenAI API, so the provider-side HTTP 400 remains an upstream-reported fact rather than an XBSTACK-measured response.

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

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:

CheckFailure risk
Unique FunctionTool.nameProvider 400 or wrong dispatch
Stable tool schemaCache invalidation and argument drift
Distinct descriptionsUnstable model selection
Approval or guardrails for high-risk toolsUnauthorized side effects
Stable tool IDs in audit logsInability to identify implementation
Reproducible dynamic enablementDifferent 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. XBSTACK has now reproduced the same ambiguity on both the original 0.19.2 fixture and current 0.22.0. A collision-safe future 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:

  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 still reproduce locally on current 0.22.0, with the same two failure modes described by the original 0.19.2 investigation:

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

The offline XBSTACK fixtures verify on both 0.19.2 and 0.22.0:

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

For the behavior reproduced here on both 0.19.2 and current 0.22.0—and for any later release that has not yet passed your own collision regression—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 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

Topic path / AI 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.

More to Explore

Topic hub →
OpenAI Agents SDK RunState: How to Resume Tool Approval Across ProcessesResume OpenAI Agents SDK Tool Approval with RunState across processes. Test to_json/from_json, approve/reject, redelivery, idempotency, and the v0.19.3 streaming fix.Implementing AI Agent Memory Systems: A 3-Layer Architecture and Practical Code to Solve Agent 'Amnesia'Build AI Agent Memory with thread state, conversation context, cross-session facts, calibrated retrieval, tenant isolation, deletion, and storage tradeoffs.AutoGen Tutorial: AgentChat, Teams, Termination, and the v0.2 Migration BoundaryAutoGen AgentChat tutorial for AssistantAgent, Teams, termination, UserProxyAgent, state persistence, and migration from legacy v0.2 APIs.LangChain v1 Tutorial: Build Agents with create_agent, Middleware, Memory, and HITLBuild a LangChain v1 agent with create_agent, middleware, memory, runtime context and HITL, replacing legacy AgentExecutor-first patterns.

AI Engineering Weekly

Production changes, real failures, experiments and new XBSTACK assets.

Comments & evidence

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.

Sign-in required Reviewed before public
Loading the discussion…