From 87a30338930e1fd452572f1e601951c7264b089e Mon Sep 17 00:00:00 2001 From: sqersters <109853788+bouclem@users.noreply.github.com> Date: Fri, 22 May 2026 07:42:22 +0200 Subject: [PATCH] Accept common XML tool name aliases (fixes #967) Some models output XML tool calls using invented tag names like `` with `` and `` instead of the canonical `` with `` and ``. Today these get rendered as plain text in the chat because the parser only matches exact tool/param tags from `availableTools()`. This adds a small alias table in `extractGrammar.ts` so the parser recognizes a few well-known wrong names and emits the canonical tool call with canonical param keys. Behavior is unchanged for compliant models. - `write_file` -> `rewrite_file` (with `path` -> `uri`, `content` -> `new_content`) - `create_file` -> `create_file_or_folder` (`path` -> `uri`) - `delete_file` -> `delete_file_or_folder` (`path` -> `uri`) Also adds one line to the XML system prompt asking the model to use the exact tool/parameter names listed and not invent new tags. --- .../contrib/void/common/prompt/prompts.ts | 1 + .../llmMessage/extractGrammar.ts | 92 ++++++++++++++++--- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index fba76815..19c0f485 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -411,6 +411,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | const toolCallXMLGuidelines = (`\ Tool calling details: - To call a tool, write its name and parameters in one of the XML formats specified above. + - Use the EXACT tool and parameter names listed above. Do not invent new tags (e.g. do not use , , or ). - After you write the tool call, you must STOP and WAIT for the result. - All parameters are REQUIRED unless noted otherwise. - You are only allowed to output ONE tool call, and it must be at the END of your response. diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 66e16791..60c390ec 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -142,6 +142,26 @@ export const extractReasoningWrapper = ( // =============== tools (XML) =============== +// Aliases for common hallucinated tool names. Some weaker models invent tags +// like instead of using the canonical , etc. We +// map a small, unambiguous set back to the real tool names so the call still +// parses. Keys are lowercase aliases, values are canonical tool names. +const toolNameAliases: Record = { + 'write_file': 'rewrite_file', + 'create_file': 'create_file_or_folder', + 'delete_file': 'delete_file_or_folder', +} + +// Per-tool parameter aliases. Models sometimes use / instead of +// the canonical /. Scoped per tool to avoid accidental +// collisions across tools that legitimately use different param names. +const paramAliasesOfTool: Partial>> = { + rewrite_file: { path: 'uri', content: 'new_content' }, + edit_file: { path: 'uri' }, + create_file_or_folder: { path: 'uri' }, + delete_file_or_folder: { path: 'uri' }, +} + const findPartiallyWrittenToolTagAtEnd = (fullText: string, toolTags: string[]) => { for (const toolTag of toolTags) { @@ -165,7 +185,12 @@ const findIndexOfAny = (fullText: string, matches: string[]) => { type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined } -const parseXMLPrefixToToolCall = (toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { +type ToolTagAliases = { + openTag: string; + closeTag: string; + paramAliases?: Record; // alias param name -> canonical param name +} +const parseXMLPrefixToToolCall = (toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName, aliases?: ToolTagAliases): RawToolCallObj => { const paramsObj: RawToolParamsObj = {} const doneParams: ToolParamName[] = [] let isDone = false @@ -190,11 +215,12 @@ const parseXMLPrefixToToolCall = (toolName: T, toolId: stri return ans } - // find first toolName tag - const openToolTag = `<${toolName}>` + // find first toolName tag (use alias tag if the model used an alias) + const openToolTag = aliases?.openTag ?? `<${toolName}>` + const closeToolTag = aliases?.closeTag ?? `` let i = str.indexOf(openToolTag) if (i === -1) return getAnswer() - let j = str.lastIndexOf(``) + let j = str.lastIndexOf(closeToolTag) if (j === -1) j = Infinity else isDone = true @@ -205,6 +231,23 @@ const parseXMLPrefixToToolCall = (toolName: T, toolId: stri const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[] if (allowedParams.length === 0) return getAnswer() + + // Build effective param tag list: canonical names first, then any aliases + // pointing at canonical params. We try them in order, so canonical wins. + const paramTagList: Array<{ openTag: string; closeTag: string; canonical: ToolParamName }> = [] + for (const paramName of allowedParams) { + paramTagList.push({ openTag: `<${paramName}>`, closeTag: ``, canonical: paramName }) + } + if (aliases?.paramAliases) { + const allowedSet = new Set(allowedParams as string[]) + for (const aliasName in aliases.paramAliases) { + const canonical = aliases.paramAliases[aliasName] + if (allowedSet.has(canonical)) { + paramTagList.push({ openTag: `<${aliasName}>`, closeTag: ``, canonical: canonical as ToolParamName }) + } + } + } + let latestMatchedOpenParam: null | ToolParamName = null let n = 0 while (true) { @@ -213,10 +256,10 @@ const parseXMLPrefixToToolCall = (toolName: T, toolId: stri // find the param name opening tag let matchedOpenParam: null | ToolParamName = null - for (const paramName of allowedParams) { - const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true) + for (const { openTag, canonical } of paramTagList) { + const removed = pm.removeFromStartUntilFullMatch(openTag, true) if (removed) { - matchedOpenParam = paramName + matchedOpenParam = canonical break } } @@ -231,14 +274,13 @@ const parseXMLPrefixToToolCall = (toolName: T, toolId: stri latestMatchedOpenParam = matchedOpenParam } - paramsObj[latestMatchedOpenParam] = '' + if (paramsObj[latestMatchedOpenParam] === undefined) paramsObj[latestMatchedOpenParam] = '' - // find the param name closing tag + // find the param name closing tag (canonical or alias) let matchedCloseParam: boolean = false let paramContents = '' - for (const paramName of allowedParams) { + for (const { closeTag } of paramTagList) { const i = pm.i - const closeTag = `` const removed = pm.removeFromStartUntilFullMatch(closeTag, true) if (removed) { const i2 = pm.i @@ -275,6 +317,27 @@ export const extractXMLToolsWrapper = ( const toolOpenTags = tools.map(t => `<${t.name}>`) for (const t of tools) { toolOfToolName[t.name] = t } + // Add alias open tags that map to a real tool. We track which canonical tool + // each alias resolves to so the parser can use canonical params. + const canonicalOfOpenTag: Record = {} + const aliasInfoOfTag: Record = {} + for (const t of tools) { + canonicalOfOpenTag[`<${t.name}>`] = t.name as ToolName + } + for (const aliasName in toolNameAliases) { + const canonical = toolNameAliases[aliasName] + if (!toolOfToolName[canonical]) continue + const openTag = `<${aliasName}>` + const closeTag = `` + toolOpenTags.push(openTag) + canonicalOfOpenTag[openTag] = canonical + aliasInfoOfTag[openTag] = { + openTag, + closeTag, + paramAliases: paramAliasesOfTool[canonical], + } + } + const toolId = generateUuid() // detect , etc @@ -282,7 +345,7 @@ export const extractXMLToolsWrapper = ( let trueFullText = '' let latestToolCall: RawToolCallObj | undefined = undefined - let foundOpenTag: { idx: number, toolName: ToolName } | null = null + let foundOpenTag: { idx: number, toolName: ToolName, openTag: string } | null = null let openToolTagBuffer = '' // the characters we've seen so far that come after a < with no space afterwards, not yet added to fullText let prevFullTextLen = 0 @@ -312,9 +375,9 @@ export const extractXMLToolsWrapper = ( const i = findIndexOfAny(fullText, toolOpenTags) if (i !== null) { const [idx, toolTag] = i - const toolName = toolTag.substring(1, toolTag.length - 1) as ToolName + const toolName = canonicalOfOpenTag[toolTag] // console.log('found ', toolName) - foundOpenTag = { idx, toolName } + foundOpenTag = { idx, toolName, openTag: toolTag } // do not count anything at or after i in fullText fullText = fullText.substring(0, idx) @@ -331,6 +394,7 @@ export const extractXMLToolsWrapper = ( toolId, trueFullText.substring(foundOpenTag.idx, Infinity), toolOfToolName, + aliasInfoOfTag[foundOpenTag.openTag], ) }