{"record":{"id":"14d69732df5e4e9a","repo":"JuliusBrussee/caveman","slug":"functions-must-map-native-tool-names-to-callables","errorCode":null,"errorMessage":"functions must map native tool names to callables","messagePattern":"functions must map native tool names to callables","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/openai.py","lineNumber":67,"sourceCode":"    _definitions: str\n\n    @property\n    def tools(self):\n        return json.loads(self._definitions)\n\n\ndef with_caveman_openai_tools(client, *, runtime, scope, protocol, tools, functions, transport=None):\n    \"\"\"Bind real application dispatch without introducing a second scheduler.\n\n    ``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=(\",\", \":\")))","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/openai.py#L49-L85","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nwith_caveman_openai_tools(..., functions={\"get_weather\": {\"url\": \"...\"}})\n// after\nwith_caveman_openai_tools(..., functions={\"get_weather\": get_weather})","handlingStrategy":"type-guard","validationCode":"if not isinstance(functions, dict) or not all(isinstance(k, str) and callable(v) for k, v in functions.items()):\n    raise TypeError(\"functions must be {str: callable}\")","typeGuard":"def is_executor_map(f): return isinstance(f, dict) and all(isinstance(k, str) and callable(v) for k, v in f.items())","tryCatchPattern":"try:\n    loop = with_caveman_openai_tools(..., functions=functions)\nexcept TypeError as e:\n    if \"callables\" in str(e):\n        functions = {k: v for k, v in functions.items() if callable(v)}\n        loop = with_caveman_openai_tools(..., functions=functions)\n    else:\n        raise","preventionTips":["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"],"tags":["python","type-mismatch","openai"],"backgroundTag":"type-mismatch","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"}