{"record":{"id":"7c431d99923afb77","repo":"JuliusBrussee/caveman","slug":"expected-native-client-function-tool-definitions","errorCode":null,"errorMessage":"Expected native client function tool definitions","messagePattern":"Expected native client function tool definitions","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/openai.py","lineNumber":72,"sourceCode":"\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=(\",\", \":\")))\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):","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/openai.py#L54-L90","documentation":"Every entry in `tools` must be a native client function tool definition: a plain dict with type=\"function\", and (for openai-chat) a nested plain `function` dict carrying a string `name`. Malformed entries raise this TypeError so the adapter can safely mirror and execute them.","triggerScenarios":"Passing tool dicts missing `type: \"function\"`, missing `name`, with a non-string name, or non-dict items (e.g. Pydantic models, JSON strings, or Anthropic-style {\"name\":..., \"input_schema\":...} definitions).","commonSituations":"Reusing tool schemas built for another SDK; hand-writing tools and forgetting the type field; passing an already-serialized tools list; passing OpenAI Response API 'custom' tools.","solutions":["Use the exact dicts returned by OpenAI's own tool-schema helpers: {\"type\":\"function\",\"function\":{\"name\":...,\"description\":...,\"parameters\":{...}}} for openai-chat","For openai-responses use flat {\"type\":\"function\",\"name\":...,\"parameters\":{...}} dicts","Convert Pydantic models via their OpenAI schema helpers before passing","Check each dict has a string `name`"],"exampleFix":"// before\ntools = [{\"name\": \"get_weather\", \"input_schema\": {...}}]  # Anthropic style\n// after\ntools = [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"parameters\": {...}}}]  # openai-chat","handlingStrategy":"validation","validationCode":"for t in tools:\n    assert isinstance(t, dict) and t.get(\"type\") == \"function\"\n    fn = t.get(\"function\", t)\n    assert isinstance(fn, dict) and isinstance(fn.get(\"name\"), str)","typeGuard":"def is_function_tool(t):\n    return isinstance(t, dict) and t.get(\"type\") == \"function\" and isinstance((t.get(\"function\") or t).get(\"name\"), str)","tryCatchPattern":"try:\n    loop = with_caveman_openai_tools(..., tools=tools, functions=functions)\nexcept TypeError as e:\n    if \"tool definitions\" in str(e):\n        tools = normalize_to_openai_function_tools(tools)\n        loop = with_caveman_openai_tools(..., tools=tools, functions=functions)\n    else:\n        raise","preventionTips":["Use OpenAI's official tool-schema helpers to build definitions","Convert Pydantic/Anthropic tool formats before passing","Snapshot-test the tools payload shape"],"tags":["python","schema","openai"],"backgroundTag":"schema-validation-failed","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"}