{"record":{"id":"461f00c7d638f2e6","repo":"JuliusBrussee/caveman","slug":"every-native-function-definition-needs-exactly-one-executor-461f00","errorCode":null,"errorMessage":"Every native function definition needs exactly one executor","messagePattern":"Every native function definition needs exactly one executor","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/openai.py","lineNumber":77,"sourceCode":"    ``protocol`` is ``openai-chat`` or ``openai-responses``. ``tools`` contains\n    the corresponding native definitions; ``functions`` maps each native tool\n    name to a callable accepting the decoded arguments dictionary.\n    \"\"\"\n    if protocol not in (\"openai-chat\", \"openai-responses\"):\n        raise ValueError(\"Expected openai-chat or openai-responses protocol\")\n    definitions = copy.deepcopy(list(tools))\n    if not plain(functions) or any(type(name) is not str or not callable(fn) for name, fn in functions.items()):\n        raise TypeError(\"functions must map native tool names to callables\")\n    names = []\n    for definition in definitions:\n        tool = definition.get(\"function\") if plain(definition) and protocol == \"openai-chat\" else definition\n        if not plain(definition) or definition.get(\"type\") != \"function\" or not plain(tool) or type(tool.get(\"name\")) is not str:\n            raise TypeError(\"Expected native client function tool definitions\")\n        names.append(tool[\"name\"])\n    if len(set(names)) != len(names) or \"caveman_retrieve\" in names or \"caveman_retrieve\" in functions:\n        raise ValueError(\"Duplicate or reserved caveman_retrieve tool name\")\n    if set(names) != set(functions):\n        raise ValueError(\"Every native function definition needs exactly one executor\")\n    if runtime.mode != \"compress\" or not in_range(__version__, \"3.10\", \"4\"):\n        return CavemanOpenAIToolLoop(with_caveman_openai(client, runtime=runtime, scope=scope, transport=transport), MappingProxyType(dict(functions)), json.dumps(definitions))\n    binding = runtime.recovery(scope)\n    tool = {\"name\": binding.name, \"description\": binding.description, \"parameters\": copy.deepcopy(binding.input_schema)}\n    definition = {\"type\": \"function\", \"function\": tool} if protocol == \"openai-chat\" else {\"type\": \"function\", **tool}\n    definitions.append(definition)\n    registry = MappingProxyType({**functions, binding.name: binding.execute})\n    registration = (protocol, binding, registry, registry[binding.name], json.dumps(definition, ensure_ascii=False, separators=(\",\", \":\")))\n    return CavemanOpenAIToolLoop(_wrap(client, runtime=runtime, scope=scope, registration=registration, transport=transport), registry,\n                                json.dumps(definitions, ensure_ascii=False, separators=(\",\", \":\")))\n\n\ndef _wrap(client, *, runtime, scope, registration=None, transport=None):\n    if not isinstance(client, (OpenAI, AsyncOpenAI)):\n        raise TypeError(\"Expected an OpenAI or AsyncOpenAI client\")\n    is_async = isinstance(client, AsyncOpenAI)\n    if not isinstance(runtime, AsyncMiddlewareRuntime if is_async else MiddlewareRuntime):\n        raise TypeError(\"Match the sync/async middleware runtime to the native client\")","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/openai.py#L59-L95","documentation":"The set of native tool definition names must exactly match the set of keys in `functions` — one executor per definition and no orphan executors. Any mismatch (missing executor or extra executor) raises this ValueError.","triggerScenarios":"Passing 3 tool definitions but a functions dict with 2 entries (or an extra executor key whose tool was not registered), or names differing by case/whitespace.","commonSituations":"Adding a tool to the definitions list and forgetting to add its executor (or vice versa); a build step filters tools but not functions; typos in the executor key.","solutions":["Ensure functions.keys() == {name of every definition}; add the missing executor or remove the extra one","Build both from a single source of truth so they cannot drift","Strip/normalize names so they match exactly"],"exampleFix":"// before\ntools = [t_search, t_docs]\nfunctions = {\"search\": search}\n// after\nfunctions = {\"search\": search, \"docs\": docs}","handlingStrategy":"validation","validationCode":"tool_names = {(t.get(\"function\") or t).get(\"name\") for t in tools}\nassert tool_names == set(functions), f\"mismatch: {tool_names ^ set(functions)}\"","typeGuard":"def executors_match(tools, functions):\n    return {(t.get(\"function\") or t).get(\"name\") for t in tools} == set(functions)","tryCatchPattern":"try:\n    loop = with_caveman_openai_tools(..., tools=tools, functions=functions)\nexcept ValueError as e:\n    if \"exactly one executor\" in str(e):\n        functions = {k: v for k, v in functions.items() if k in tool_names}\n        loop = with_caveman_openai_tools(..., tools=tools, functions=functions)\n    else:\n        raise","preventionTips":["Derive functions and tools from a single registry so they cannot drift","Assert set equality in tests before calling the adapter","When filtering tools, filter functions with the same predicate"],"tags":["python","validation","openai"],"backgroundTag":"invalid-argument-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}