diff --git a/.github/images/tool-miscall-loop.png b/.github/images/tool-miscall-loop.png new file mode 100644 index 00000000000..2ce94eceaf5 Binary files /dev/null and b/.github/images/tool-miscall-loop.png differ diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 261731b8b0a..c933bee3b2e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1190,6 +1190,10 @@ export namespace Config { .positive() .optional() .describe("Timeout in milliseconds for model context protocol (MCP) requests"), + tool_aliases: z + .record(z.string(), z.string()) + .optional() + .describe("Map of alias → canonical tool name for repairing miscalled tools"), }) .optional(), }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index fa880391276..7ebff408160 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -180,21 +180,51 @@ export namespace LLM { }) }, async experimental_repairToolCall(failed) { - const lower = failed.toolCall.toolName.toLowerCase() - if (lower !== failed.toolCall.toolName && tools[lower]) { + const name = failed.toolCall.toolName + // try lowercase first + if (name !== name.toLowerCase() && tools[name.toLowerCase()]) { l.info("repairing tool call", { - tool: failed.toolCall.toolName, - repaired: lower, + tool: name, + repaired: name.toLowerCase(), }) - return { - ...failed.toolCall, - toolName: lower, + return { ...failed.toolCall, toolName: name.toLowerCase() } + } + // try stripping underscores/hyphens and case-insensitive match + // handles todo_write -> todowrite, Web_Fetch -> webfetch, etc. + const normalized = name.replace(/[-_]/g, "").toLowerCase() + for (const toolName of Object.keys(tools)) { + if (toolName.toLowerCase() === normalized) { + l.info("repairing tool call", { + tool: name, + repaired: toolName, + }) + return { ...failed.toolCall, toolName } } } + // try alias lookup (config overrides builtins) + const builtinAliases: Record = { + search: "grep", + find: "glob", + cat: "read", + run: "bash", + shell: "bash", + todo: "todowrite", + fetch: "webfetch", + } + const userAliases = cfg.experimental?.tool_aliases ?? {} + const aliases = { ...builtinAliases, ...userAliases } + const aliasTarget = aliases[name] ?? aliases[name.toLowerCase()] ?? aliases[normalized] + if (aliasTarget && tools[aliasTarget]) { + l.info("repairing tool call via alias", { + tool: name, + alias: aliasTarget, + }) + return { ...failed.toolCall, toolName: aliasTarget } + } return { ...failed.toolCall, input: JSON.stringify({ - tool: failed.toolCall.toolName, + tool: name, error: failed.error.message, }), toolName: "invalid",