JuliusBrussee/caveman · error · TypeError

Duplicate or reserved caveman_retrieve tool name

Error message

Duplicate or reserved caveman_retrieve tool name

What it means

`withCavemanOpenAITools` wraps your OpenAI client and reserves the tool name `caveman_retrieve` for its own recovery tool. In packages/middleware/typescript/src/openai.ts:32 it throws a TypeError if your supplied tool definitions contain duplicate names, an empty/invalid name, or any tool (or function executor key) named `caveman_retrieve`, since that would collide with the reserved name.

Solutions

  1. Remove or rename any user-supplied `caveman_retrieve` tool/function; let the wrapper provide it.
  2. Deduplicate the tools array before wrapping (unique, non-empty names).
  3. Verify `functions` is a plain object of name -> function with no extra keys.

Example fix

// before
const tools = [...baseTools, { type: 'function', function: { name: 'caveman_retrieve', ... } }];
// after
const tools = baseTools; // caveman_retrieve is injected by withCavemanOpenAITools
Defensive patterns

Strategy: validation

Validate before calling

function assertNoReservedTools(tools: { name?: string; function?: { name?: string } }[], functions: Record<string, unknown>) {
  const names = tools.map(t => 'function' in t ? t.function?.name : t.name);
  if (names.some(n => typeof n !== 'string' || !n)) throw new Error('All tools need non-empty string names');
  if (new Set(names).size !== names.length) throw new Error('Duplicate tool names');
  if (names.includes('caveman_retrieve') || 'caveman_retrieve' in functions) throw new Error('caveman_retrieve is reserved');
}

Type guard

const isPlainFunctions = (f: unknown): f is Record<string, (...a: unknown[]) => unknown> =>
  typeof f === 'object' && f !== null && !Array.isArray(f) && Object.values(f).every(v => typeof v === 'function');

Try / catch

try {
  const loop = withCavemanOpenAITools(client, options);
} catch (e) {
  if (e instanceof TypeError && /reserved caveman_retrieve/.test(e.message)) {
    console.error('Remove your own caveman_retrieve tool; the wrapper injects it.');
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `withCavemanOpenAITools(client, { protocol, tools, functions })` where: two tool definitions share a name; a tool has an empty or non-string name; a tool is named `caveman_retrieve`; or `options.functions` already has a `caveman_retrieve` key; or `options.functions` is not a plain object.

Common situations: An app that previously registered its own caveman retrieval tool and now wraps with this library; programmatic tool generation producing duplicates; merging tool lists from multiple sources.

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


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/622187e834fb5841. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/typescript/src/openai.ts:32

}

type Functions = Readonly<Record<string, (input: unknown) => unknown | Promise<unknown>>>;
export interface OpenAIToolLoop<T extends OpenAI, Tool> {
  readonly client: T;
  /** A fresh native definition list; changing it cannot change registration. */
  readonly tools: Tool[];
  /** Use this immutable table for every application-owned function dispatch. */
  readonly functions: Functions;
}
type ChatFunction = OpenAI.Chat.Completions.ChatCompletionFunctionTool;
type ResponseFunction = OpenAI.Responses.FunctionTool;
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat';tools:ChatFunction[];functions:Functions}):OpenAIToolLoop<T,ChatFunction>;
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-responses';tools:ResponseFunction[];functions:Functions}):OpenAIToolLoop<T,ResponseFunction>;
/** Native application-owned Chat/Responses calls, with no added scheduler. */
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat'|'openai-responses';tools:(ChatFunction|ResponseFunction)[];functions:Functions}):OpenAIToolLoop<T,ChatFunction|ResponseFunction>{
  const definitions=structuredClone(options.tools);
  const names=definitions.map(tool=>'function' in tool?tool.function.name:tool.name);
  if(!plain(options.functions)||names.some(name=>typeof name!=='string'||!name)||new Set(names).size!==names.length||names.includes('caveman_retrieve')||'caveman_retrieve' in options.functions)throw new TypeError('Duplicate or reserved caveman_retrieve tool name');
  if(names.length!==Object.keys(options.functions).length||names.some(name=>typeof options.functions[name]!=='function'))throw new TypeError('Every native function definition needs exactly one executor');
  let functions=Object.freeze({...options.functions});
  let context:RecoveryContext|undefined;
  if(options.runtime.mode==='compress'&&frameworkCompatible('openai',VERSION)){
    const binding=options.runtime.recovery(options.scope);
    const schema={name:binding.name,description:binding.description,parameters:binding.inputSchema};
    const definition=options.protocol==='openai-chat'?{type:'function' as const,function:schema}:{type:'function' as const,...schema,strict:false};
    definitions.push(definition);
    const execute=(input:unknown)=>binding.execute(input as never);
    functions=Object.freeze({...functions,[binding.name]:execute});
    context={runtime:options.runtime,scope:options.scope,binding,overhead:JSON.stringify(definition),logicalCallId:crypto.randomUUID(),
      isRegistered:()=>options.runtime.ownsBinding(binding,options.scope)&&functions[binding.name]===execute};
  }
  const serialized=JSON.stringify(definitions);
  return Object.freeze({client:wrapOpenAI(client,options,context,options.protocol),functions,get tools(){return JSON.parse(serialized);}});
}

/** A native withOptions clone; APIPromise, parsers, streams and runners survive. */

View on GitHub (pinned to 3ee70a1026)