JuliusBrussee/caveman · error · TypeError
functions must map native tool names to callables
Error message
functions must map native tool names to callables
What it means
The `functions` argument must be a plain mapping whose keys are native tool name strings and whose values are callables that execute each tool. Anything else — non-dict, dict with non-string keys, or non-callable values — raises this TypeError.
Solutions
- Pass a dict mapping each native tool name to a callable, e.g. {"get_weather": get_weather}
- Ensure every value is callable (functions, lambdas, bound methods, functools.partial)
- Ensure keys are exactly the native tool name strings
Example fix
// before
with_caveman_openai_tools(..., functions={"get_weather": {"url": "..."}})
// after
with_caveman_openai_tools(..., functions={"get_weather": get_weather}) Defensive patterns
Strategy: type-guard
Validate before calling
if not isinstance(functions, dict) or not all(isinstance(k, str) and callable(v) for k, v in functions.items()):
raise TypeError("functions must be {str: callable}") Type guard
def is_executor_map(f): return isinstance(f, dict) and all(isinstance(k, str) and callable(v) for k, v in f.items())
Try / catch
try:
loop = with_caveman_openai_tools(..., functions=functions)
except TypeError as e:
if "callables" in str(e):
functions = {k: v for k, v in functions.items() if callable(v)}
loop = with_caveman_openai_tools(..., functions=functions)
else:
raise Prevention
- Map tool names to plain functions, not dicts/config objects
- Lint for executor maps built from dynamic data
- Add a unit test asserting every executor is callable
When it happens
Trigger: Passing a list of handlers, a dict whose values are coroutines mistaken for callables while being non-callable objects, None, or a MappingProxyType-like object failing the plain() check; also values like strings or dicts where callables were intended.
Common situations: Building the executor map dynamically and a tool name maps to a placeholder (e.g. None or a config dict); passing class objects instead of instances' bound methods; passing functools.partial misconfigured.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Expected an OpenAI or AsyncOpenAI client
- Match the sync/async Caveman transport to the native client
- Match the sync/async middleware runtime to the native client
- ASGI context must come from authenticated server state
- Duplicate or reserved caveman_retrieve tool name
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/14d69732df5e4e9a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/openai.py:67
_definitions: str
@property
def tools(self):
return json.loads(self._definitions)
def with_caveman_openai_tools(client, *, runtime, scope, protocol, tools, functions, transport=None):
"""Bind real application dispatch without introducing a second scheduler.
``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=(",", ":")))View on GitHub (pinned to 3ee70a1026)