JuliusBrussee/caveman · error · ValueError
Every native function definition needs exactly one executor
Error message
Every native function definition needs exactly one executor
What it means
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.
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
Example fix
// before
tools = [t_search, t_docs]
functions = {"search": search}
// after
functions = {"search": search, "docs": docs} Defensive patterns
Strategy: validation
Validate before calling
tool_names = {(t.get("function") or t).get("name") for t in tools}
assert tool_names == set(functions), f"mismatch: {tool_names ^ set(functions)}" Type guard
def executors_match(tools, functions):
return {(t.get("function") or t).get("name") for t in tools} == set(functions) Try / catch
try:
loop = with_caveman_openai_tools(..., tools=tools, functions=functions)
except ValueError as e:
if "exactly one executor" in str(e):
functions = {k: v for k, v in functions.items() if k in tool_names}
loop = with_caveman_openai_tools(..., tools=tools, functions=functions)
else:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Expected openai-chat or openai-responses protocol
- artifact_id is required
- ASGI context resolver or request bounds are invalid
- assemble requires model and session_id
- assembly slot id must be non-empty and unique
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/461f00c7d638f2e6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/openai.py:77
``protocol`` is ``openai-chat`` or ``openai-responses``. ``tools`` contains
the corresponding native definitions; ``functions`` maps each native tool
name to a callable accepting the decoded arguments dictionary.
"""
if protocol not in ("openai-chat", "openai-responses"):
raise ValueError("Expected openai-chat or openai-responses protocol")
definitions = copy.deepcopy(list(tools))
if not plain(functions) or any(type(name) is not str or not callable(fn) for name, fn in functions.items()):
raise TypeError("functions must map native tool names to callables")
names = []
for definition in definitions:
tool = definition.get("function") if plain(definition) and protocol == "openai-chat" else definition
if not plain(definition) or definition.get("type") != "function" or not plain(tool) or type(tool.get("name")) is not str:
raise TypeError("Expected native client function tool definitions")
names.append(tool["name"])
if len(set(names)) != len(names) or "caveman_retrieve" in names or "caveman_retrieve" in functions:
raise ValueError("Duplicate or reserved caveman_retrieve tool name")
if set(names) != set(functions):
raise ValueError("Every native function definition needs exactly one executor")
if runtime.mode != "compress" or not in_range(__version__, "3.10", "4"):
return CavemanOpenAIToolLoop(with_caveman_openai(client, runtime=runtime, scope=scope, transport=transport), MappingProxyType(dict(functions)), json.dumps(definitions))
binding = runtime.recovery(scope)
tool = {"name": binding.name, "description": binding.description, "parameters": copy.deepcopy(binding.input_schema)}
definition = {"type": "function", "function": tool} if protocol == "openai-chat" else {"type": "function", **tool}
definitions.append(definition)
registry = MappingProxyType({**functions, binding.name: binding.execute})
registration = (protocol, binding, registry, registry[binding.name], json.dumps(definition, ensure_ascii=False, separators=(",", ":")))
return CavemanOpenAIToolLoop(_wrap(client, runtime=runtime, scope=scope, registration=registration, transport=transport), registry,
json.dumps(definitions, ensure_ascii=False, separators=(",", ":")))
def _wrap(client, *, runtime, scope, registration=None, transport=None):
if not isinstance(client, (OpenAI, AsyncOpenAI)):
raise TypeError("Expected an OpenAI or AsyncOpenAI client")
is_async = isinstance(client, AsyncOpenAI)
if not isinstance(runtime, AsyncMiddlewareRuntime if is_async else MiddlewareRuntime):
raise TypeError("Match the sync/async middleware runtime to the native client")View on GitHub (pinned to 3ee70a1026)