JuliusBrussee/caveman · error · ValueError
Duplicate or reserved caveman_retrieve tool name
Error message
Duplicate or reserved caveman_retrieve tool name
What it means
Tool names must be unique, and the reserved name `caveman_retrieve` may not be used by caller-supplied tools or executors because the adapter injects its own caveman_retrieve tool in compress mode. Duplicates or use of the reserved name raise ValueError.
Solutions
- Rename duplicate tools so every native definition has a unique name
- Rename any caller tool/executor away from `caveman_retrieve`; it is reserved by the adapter
- Deduplicate the tools list before calling (e.g. {t["function"]["name"]: t for t in tools}.values())
Example fix
// before
functions = {"search": search, "caveman_retrieve": my_retrieve}
// after
functions = {"search": search, "docs_lookup": my_retrieve} Defensive patterns
Strategy: validation
Validate before calling
names = [t.get("function", t).get("name") for t in tools]
assert len(set(names)) == len(names) and "caveman_retrieve" not in names and "caveman_retrieve" not in functions Type guard
def names_are_unique_and_unreserved(tools, functions):
names = [(t.get("function") or t).get("name") for t in tools]
return len(set(names)) == len(names) and "caveman_retrieve" not in names and "caveman_retrieve" not in functions Try / catch
try:
loop = with_caveman_openai_tools(..., tools=tools, functions=functions)
except ValueError as e:
if "caveman_retrieve" in str(e) or "Duplicate" in str(e):
seen, deduped = set(), []
for t in tools:
n = (t.get("function") or t).get("name")
if n not in seen:
seen.add(n); deduped.append(t)
tools = deduped
loop = with_caveman_openai_tools(..., tools=tools, functions=functions)
else:
raise Prevention
- Never name tools caveman_retrieve; it is reserved
- Deduplicate generated tools before registration
- Prefix tool names with your feature to avoid collisions
When it happens
Trigger: Passing two native definitions with the same `name`, or a tool/executor named `caveman_retrieve`, in with_caveman_openai_tools.
Common situations: Generating tools programmatically where several endpoints share a name; migrating code that already defined its own retrieval helper called caveman_retrieve from an earlier integration.
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
- Every native function definition needs exactly one executor
- Expected an OpenAI or AsyncOpenAI client
- Expected native client function tool definitions
- Expected openai-chat or openai-responses protocol
- functions must map native tool names to callables
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/2738fa9e8d2bf3ce.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/openai.py:75
"""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=(",", ":")))
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)View on GitHub (pinned to 3ee70a1026)