{"record":{"id":"b7383b148db1ed1b","repo":"JuliusBrussee/caveman","slug":"every-native-function-definition-needs-exactly-one-executor","errorCode":null,"errorMessage":"Every native function definition needs exactly one executor","messagePattern":"Every native function definition needs exactly one executor","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/typescript/src/openai.ts","lineNumber":33,"sourceCode":"\ntype Functions = Readonly<Record<string, (input: unknown) => unknown | Promise<unknown>>>;\nexport interface OpenAIToolLoop<T extends OpenAI, Tool> {\n  readonly client: T;\n  /** A fresh native definition list; changing it cannot change registration. */\n  readonly tools: Tool[];\n  /** Use this immutable table for every application-owned function dispatch. */\n  readonly functions: Functions;\n}\ntype ChatFunction = OpenAI.Chat.Completions.ChatCompletionFunctionTool;\ntype ResponseFunction = OpenAI.Responses.FunctionTool;\nexport function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat';tools:ChatFunction[];functions:Functions}):OpenAIToolLoop<T,ChatFunction>;\nexport function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-responses';tools:ResponseFunction[];functions:Functions}):OpenAIToolLoop<T,ResponseFunction>;\n/** Native application-owned Chat/Responses calls, with no added scheduler. */\nexport function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat'|'openai-responses';tools:(ChatFunction|ResponseFunction)[];functions:Functions}):OpenAIToolLoop<T,ChatFunction|ResponseFunction>{\n  const definitions=structuredClone(options.tools);\n  const names=definitions.map(tool=>'function' in tool?tool.function.name:tool.name);\n  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');\n  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');\n  let functions=Object.freeze({...options.functions});\n  let context:RecoveryContext|undefined;\n  if(options.runtime.mode==='compress'&&frameworkCompatible('openai',VERSION)){\n    const binding=options.runtime.recovery(options.scope);\n    const schema={name:binding.name,description:binding.description,parameters:binding.inputSchema};\n    const definition=options.protocol==='openai-chat'?{type:'function' as const,function:schema}:{type:'function' as const,...schema,strict:false};\n    definitions.push(definition);\n    const execute=(input:unknown)=>binding.execute(input as never);\n    functions=Object.freeze({...functions,[binding.name]:execute});\n    context={runtime:options.runtime,scope:options.scope,binding,overhead:JSON.stringify(definition),logicalCallId:crypto.randomUUID(),\n      isRegistered:()=>options.runtime.ownsBinding(binding,options.scope)&&functions[binding.name]===execute};\n  }\n  const serialized=JSON.stringify(definitions);\n  return Object.freeze({client:wrapOpenAI(client,options,context,options.protocol),functions,get tools(){return JSON.parse(serialized);}});\n}\n\n/** A native withOptions clone; APIPromise, parsers, streams and runners survive. */\nexport function withCavemanOpenAI<T extends OpenAI>(client:T,options:OpenAIOptions):T{","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/typescript/src/openai.ts#L15-L51","documentation":"This wrapper enforces a strict 1:1 mapping between tool definitions and executor functions. In packages/middleware/typescript/src/openai.ts:33 it throws a TypeError when the number of tool names differs from the number of keys in `options.functions`, or when any name in `tools` has no corresponding function executor (or the executor is not a function).","triggerScenarios":"Calling `withCavemanOpenAITools` where `Object.keys(options.functions).length !== tools.length`, or `typeof options.functions[someToolName] !== 'function'` (undefined or non-callable).","commonSituations":"Adding a tool definition but forgetting to add its executor; typos in function keys vs tool names; passing executor maps built for a different protocol variant; refactors renaming a tool but not its key.","solutions":["Add an executor function in `functions` for every tool name in `tools`.","Remove definitions for tools you have no executor for.","Generate `functions` from the same source as `tools` so keys stay in sync."],"exampleFix":"// before\nwithCavemanOpenAITools(client, { protocol: 'openai-chat', tools: [getWeatherDef], functions: {} });\n// after\nwithCavemanOpenAITools(client, { protocol: 'openai-chat', tools: [getWeatherDef], functions: { getWeather: async (args) => fetchWeather(args) } });","handlingStrategy":"validation","validationCode":"function assertExecutorsMatch(tools: { name?: string; function?: { name?: string } }[], functions: Record<string, unknown>) {\n  const names = tools.map(t => 'function' in t ? t.function?.name : t.name) as string[];\n  const missing = names.filter(n => typeof functions[n] !== 'function');\n  if (names.length !== Object.keys(functions).length || missing.length) {\n    throw new Error(`Executor mismatch; missing: ${missing.join(', ')}`);\n  }\n}","typeGuard":"const hasExecutorFor = (functions: Record<string, unknown>, name: string): functions is Record<string, Function> =>\n  typeof functions[name] === 'function';","tryCatchPattern":"try {\n  const loop = withCavemanOpenAITools(client, options);\n} catch (e) {\n  if (e instanceof TypeError && /exactly one executor/.test(e.message)) {\n    console.error('tools and functions must be 1:1; check names and keys.');\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Generate the executor map from the same array of tool definitions so they never drift.","Key executor objects by the exact tool name constant shared with the definition.","Type the functions param as Record<ToolName, Fn> so TypeScript flags missing keys."],"tags":["openai","tool-registration","mismatch","validation"],"backgroundTag":"missing-required-argument","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"}