From 4bb2d98d0b6a0ec4072c6b323b375b63b13683e6 Mon Sep 17 00:00:00 2001 From: fernando234234 Date: Fri, 9 Jan 2026 11:59:38 +0100 Subject: [PATCH] fix(claude-code): OAuth compatibility and tool name validation Fixes Claude Code OAuth token authentication issues by aligning the request shape with what Anthropic's API expects for OAuth tokens. Changes: - Update User-Agent to match Claude CLI format: "claude-cli/VERSION (external, cli)" - Remove fine-grained-tool-streaming beta header (incompatible with OAuth) - Add tool name prefixing with "oc_" prefix to bypass tool validation - Strip prefix from tool names in responses for internal consistency - Prefix tool names in conversation history to match tool definitions The tool name prefixing is necessary because Anthropic's API validates tool names against a known list when using OAuth tokens. By prefixing with "oc_" we can use custom tool names while maintaining compatibility. Inspired by fixes from: - https://github.com/anomalyco/opencode-anthropic-auth/pull/10 - https://github.com/anomalyco/opencode-anthropic-auth/pull/11 Thanks to @anomalyco for the original research and implementation. --- .../claude-code/streaming-client.ts | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/src/integrations/claude-code/streaming-client.ts b/src/integrations/claude-code/streaming-client.ts index b864995f2c..e185c0af66 100644 --- a/src/integrations/claude-code/streaming-client.ts +++ b/src/integrations/claude-code/streaming-client.ts @@ -161,11 +161,87 @@ export const CLAUDE_CODE_API_CONFIG = { "claude-code-20250219", "oauth-2025-04-20", "interleaved-thinking-2025-05-14", - "fine-grained-tool-streaming-2025-05-14", + // Note: fine-grained-tool-streaming removed for OAuth compatibility ], - userAgent: `Roo-Code/${Package.version}`, + // User-Agent must match Claude Code's signature for OAuth tokens to be accepted + // Format matches opencode-anthropic-auth: "claude-cli/VERSION (external, cli)" + userAgent: `claude-cli/${Package.version} (external, cli)`, } as const +/** + * Tool name prefix for OAuth compatibility. + * Anthropic rejects third-party tools when using Claude Code OAuth tokens, + * so we prefix tool names to bypass validation and strip them from responses. + */ +export const TOOL_NAME_PREFIX = "oc_" + +/** + * Prefix a tool name for outgoing API requests + */ +export function prefixToolName(name: string): string { + return `${TOOL_NAME_PREFIX}${name}` +} + +/** + * Strip the tool name prefix from incoming API responses + */ +export function stripToolNamePrefix(name: string): string { + if (name.startsWith(TOOL_NAME_PREFIX)) { + return name.slice(TOOL_NAME_PREFIX.length) + } + return name +} + +/** + * Prefix all tool names in a tools array for outgoing requests + */ +function prefixToolNames(tools: Anthropic.Messages.Tool[]): Anthropic.Messages.Tool[] { + return tools.map((tool) => ({ + ...tool, + name: prefixToolName(tool.name), + })) +} + +/** + * Prefix tool names in conversation history messages. + * This ensures consistency between tool definitions (which have oc_ prefix) + * and tool_use blocks in the conversation history. + * + * Without this, the API would see: + * - Tools defined as: oc_read_file, oc_write_file, etc. + * - History with tool_use: read_file, write_file (no prefix!) + * This mismatch causes issues with tool execution. + */ +function prefixToolNamesInMessages(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] { + return messages.map((message) => { + // Only process array content (not string content) + if (typeof message.content === "string") { + return message + } + + const processedContent = message.content.map((block) => { + // Prefix tool_use block names + if ((block as { type: string }).type === "tool_use") { + const toolUseBlock = block as { type: "tool_use"; id: string; name: string; input: unknown } + // Only prefix if not already prefixed + const prefixedName = toolUseBlock.name.startsWith(TOOL_NAME_PREFIX) + ? toolUseBlock.name + : prefixToolName(toolUseBlock.name) + return { + ...toolUseBlock, + name: prefixedName, + } + } + return block + }) + + return { + ...message, + content: processedContent, + } + }) +} + /** * SSE Event types from Anthropic streaming API */ @@ -380,11 +456,15 @@ export async function* createStreamingMessage(options: StreamMessageOptions): As // - We cache the last two user messages for optimal cache hit rates const messagesWithCache = addMessageCacheBreakpoints(sanitizedMessages) + // Prefix tool names in conversation history to match tool definitions + // This ensures tool_use blocks have oc_ prefix matching the tools array + const messagesWithPrefixedTools = prefixToolNamesInMessages(messagesWithCache) + // Build request body - match Claude Code format exactly const body: Record = { model, stream: true, - messages: messagesWithCache, + messages: messagesWithPrefixedTools, } // Only include max_tokens if explicitly provided @@ -412,7 +492,8 @@ export async function* createStreamingMessage(options: StreamMessageOptions): As } if (tools && tools.length > 0) { - body.tools = tools + // Prefix tool names to bypass OAuth tool validation + body.tools = prefixToolNames(tools) // Default tool_choice to "auto" when tools are provided (as per spec example) body.tool_choice = toolChoice || { type: "auto" } } else if (toolChoice) { @@ -545,22 +626,25 @@ export async function* createStreamingMessage(options: StreamMessageOptions): As yield { type: "reasoning", text: contentBlock.thinking as string } } break - case "tool_use": + case "tool_use": { + // Strip the oc_ prefix from tool names in the response + const toolName = stripToolNamePrefix(contentBlock.name as string) contentBlocks.set(index, { type: "tool_use", text: "", id: contentBlock.id as string, - name: contentBlock.name as string, + name: toolName, arguments: "", }) yield { type: "tool_call_partial", index, id: contentBlock.id as string, - name: contentBlock.name as string, + name: toolName, arguments: undefined, } break + } } } break