Skip to content

responses.parse() matches a namespaced function call to a top-level tool by name alone #4064

Open

Description

@chrikrah

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

responses.parse() looks up the tool for each function_call by name alone, and only among top-level tools. get_input_tool_by_name in src/openai/lib/_parsing/_responses.py:171 checks tool["type"] == "function" and never reads function_call.namespace. For a call to crm.lookup:

  • With the tool only inside a namespace tool, parsed_arguments is None, so the pydantic_responses_function_tool model is never applied.
  • With a top-level function also named lookup, the call is validated against that other tool's model, and parse() raises ValidationError on a well-formed response.

The second is the costly case, since a namespace exists so two groups can reuse a function name.

To Reproduce

  1. Build a namespace tool crm holding pydantic_responses_function_tool(CrmLookup, name="lookup").
  2. Optionally add a top-level pydantic_responses_function_tool(CatalogLookup, name="lookup").
  3. Call client.responses.parse(...) against a response whose output is a function_call with name="lookup", namespace="crm".

Code snippets

# probe_ns2.py: httpx2.MockTransport serves the response, so no network
import json, httpx2, openai, pydantic
from openai import OpenAI
class CrmLookup(pydantic.BaseModel):
    customer_id: str
class CatalogLookup(pydantic.BaseModel):
    sku: int
def client_for(ns, 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": ns, "arguments": json.dumps(args)}]})
    return OpenAI(api_key="x", http_client=httpx2.Client(transport=httpx2.MockTransport(respond)))
crm = {"type": "namespace", "name": "crm", "description": "CRM tools",
       "tools": [openai.pydantic_responses_function_tool(CrmLookup, name="lookup")]}
catalog_lookup = openai.pydantic_responses_function_tool(CatalogLookup, name="lookup")
deferred = openai.pydantic_responses_function_tool(CatalogLookup, name="lookup", defer_loading=True)
cases = [("crm.lookup, namespace only", "crm", {"customer_id": "C-42"}, [crm]),
         ("crm.lookup, plus top-level lookup", "crm", {"customer_id": "C-42"}, [crm, catalog_lookup]),
         ("deferred top-level (namespace == name)", "lookup", {"sku": 7}, [{"type": "tool_search"}, deferred])]
for label, ns, args, tools in cases:
    try:
        r = client_for(ns, args).responses.parse(model="gpt-5", input="hi", tools=tools)
        print(f"{label}: {r.output[0].parsed_arguments!r}")
    except Exception as e:
        print(f"{label}: {type(e).__name__}: {str(e).splitlines()[0]}")
$ python -I probe_ns2.py      # openai-python 5e7a23e (3.29.0), pydantic 2.13.5
crm.lookup, namespace only: None
crm.lookup, plus top-level lookup: ValidationError: 1 validation error for CatalogLookup
deferred top-level (namespace == name): CatalogLookup(sku=7)
# with a local patch that searches the matching namespace tool first:
crm.lookup, namespace only: CrmLookup(customer_id='C-42')
crm.lookup, plus top-level lookup: CrmLookup(customer_id='C-42')
deferred top-level (namespace == name): CatalogLookup(sku=7)
# not run: responses.stream()

I'd scope the lookup: when function_call.namespace names a namespace tool, search only its tools, and otherwise search top-level functions as today. The fallback matters, because a deferred top-level tool from #4029 comes back with namespace == name and parses correctly now. get_input_tool_by_name in _responses.py has one caller, so it can take namespace directly.

OS

Ubuntu 24.04.5 LTS

Python version

Python 3.12.6

Library version

openai 3.29.0 at 5e7a23e

@jbeckwith-oai should a namespace that names no namespace tool keep falling back to top-level tools, as the deferred case needs? If that matches what you intended for #4029, I can paste the patch here as a diff.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions