Repository navigation
responses.parse() matches a namespaced function call to a top-level tool by name alone #4064
Description
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
namespacetool,parsed_argumentsisNone, so thepydantic_responses_function_toolmodel is never applied. - With a top-level function also named
lookup, the call is validated against that other tool's model, andparse()raisesValidationErroron a well-formed response.
The second is the costly case, since a namespace exists so two groups can reuse a function name.
To Reproduce
- Build a
namespacetoolcrmholdingpydantic_responses_function_tool(CrmLookup, name="lookup"). - Optionally add a top-level
pydantic_responses_function_tool(CatalogLookup, name="lookup"). - Call
client.responses.parse(...)against a response whose output is afunction_callwithname="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