diff --git a/src/api/providers/claude-code.ts b/src/api/providers/claude-code.ts index db7eaae522..bfce6f78fd 100644 --- a/src/api/providers/claude-code.ts +++ b/src/api/providers/claude-code.ts @@ -21,6 +21,47 @@ import { ApiHandlerOptions } from "../../shared/api" import { countTokens } from "../../utils/countTokens" import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters" +/** + * IMPORTANT: + * Claude Code OAuth (sk-ant-oat...) gateway can reject requests when a user-defined tool name + * collides with a reserved/system tool name on their side (observed for "read_file"). + * + * Symptom: the request fails only when a tool is named "read_file", + * but starts working if renamed (e.g. "_read_file"). + * + * Workaround: namespace reserved tool names before sending the request, and map tool call names back + * so the rest of Roo’s tool routing remains unchanged. + */ +const RESERVED_ANTHROPIC_TOOL_NAMES = new Set(["read_file"]) +const RESERVED_ANTHROPIC_TOOL_PREFIX = "_" + +function patchReservedAnthropicToolNames( + tools: T[], +): { + tools: T[] + reverseNameMap: Map +} { + const reverseNameMap = new Map() + + const patchedTools = tools.map((tool) => { + if (!RESERVED_ANTHROPIC_TOOL_NAMES.has(tool.name)) { + return tool + } + + const patchedName = `${RESERVED_ANTHROPIC_TOOL_PREFIX}${tool.name}` + reverseNameMap.set(patchedName, tool.name) + + // Keep the tool shape unchanged, only patch the name + return { ...tool, name: patchedName } as T + }) + + return { tools: patchedTools, reverseNameMap } +} + +function unpatchToolName(name: string, reverseNameMap: Map): string { + return reverseNameMap.get(name) ?? name +} + /** * Converts OpenAI tool_choice to Anthropic ToolChoice format * @param toolChoice - OpenAI tool_choice parameter @@ -144,8 +185,19 @@ export class ClaudeCodeHandler implements ApiHandler, SingleCompletionHandler { // Generate user_id metadata in the format required by Claude Code API const userId = generateUserId(email || undefined) - const anthropicTools = convertOpenAIToolsToAnthropic(metadata?.tools ?? []) - const anthropicToolChoice = convertOpenAIToolChoice(metadata?.tool_choice, metadata?.parallelToolCalls) + const rawAnthropicTools = convertOpenAIToolsToAnthropic(metadata?.tools ?? []) + let anthropicToolChoice = convertOpenAIToolChoice(metadata?.tool_choice, metadata?.parallelToolCalls) + + // Work around reserved tool names in Claude Code OAuth gateway (e.g. "read_file") + const { tools: anthropicTools, reverseNameMap } = patchReservedAnthropicToolNames(rawAnthropicTools) + + // If toolChoice targets a reserved tool, patch it to match the renamed tool list. + if (anthropicToolChoice?.type === "tool" && RESERVED_ANTHROPIC_TOOL_NAMES.has(anthropicToolChoice.name)) { + anthropicToolChoice = { + ...anthropicToolChoice, + name: `${RESERVED_ANTHROPIC_TOOL_PREFIX}${anthropicToolChoice.name}`, + } + } // Determine reasoning effort and thinking configuration const reasoningLevel = this.getReasoningEffort(model.info) @@ -226,7 +278,8 @@ export class ClaudeCodeHandler implements ApiHandler, SingleCompletionHandler { type: "tool_call_partial", index: chunk.index, id: chunk.id, - name: chunk.name, + // chunk.name is expected to always exist for tool calls + name: unpatchToolName(chunk.name as string, reverseNameMap), arguments: chunk.arguments, } break