/** * Request transformation: Anthropic Messages → OpenAI Chat Completions * Derived from cc-switch (https://github.com/farion1231/cc-switch) * Original work by Jason Young, MIT License */ import type { AnthropicRequest, AnthropicContentBlock, AnthropicMessage, OpenAIChatRequest, OpenAIChatMessage, OpenAIChatContentPart, OpenAIToolCall, OpenAITool, } from './types.js' /** * Convert Anthropic Messages request to OpenAI Chat Completions request. */ export function anthropicToOpenaiChat( body: AnthropicRequest, options: { roundTripReasoningContent?: boolean; passThinkingToggle?: boolean } = {}, ): OpenAIChatRequest { const messages: OpenAIChatMessage[] = [] // Convert system prompt if (body.system) { if (typeof body.system === 'string') { messages.push({ role: 'system', content: body.system }) } else if (Array.isArray(body.system)) { const text = body.system.map((b) => b.text).join('\n') messages.push({ role: 'system', content: text }) } } // Convert messages for (const msg of body.messages) { convertMessage(msg, messages, options) } // Build request const result: OpenAIChatRequest = { model: body.model, messages, stream: body.stream, } // max_tokens — omit to let upstream provider use its own default/max. // Claude Code sends very large values (e.g. 128K) that exceed many // providers' limits (DeepSeek: 8192, etc.). // temperature & top_p if (body.temperature !== undefined) result.temperature = body.temperature if (body.top_p !== undefined) result.top_p = body.top_p // stop_sequences → stop if (body.stop_sequences && body.stop_sequences.length > 0) { result.stop = body.stop_sequences } // tools if (body.tools && body.tools.length > 0) { result.tools = body.tools .filter((t) => t.name !== 'BatchTool') .map((t): OpenAITool => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.input_schema, }, })) } // tool_choice if (body.tool_choice !== undefined) { result.tool_choice = convertToolChoice(body.tool_choice) } // thinking → reasoning_effort if (body.thinking) { const budget = body.thinking.budget_tokens if (budget !== undefined) { if (budget <= 1024) result.reasoning_effort = 'low' else if (budget <= 8192) result.reasoning_effort = 'medium' else result.reasoning_effort = 'high' } else if (body.thinking.type === 'enabled') { result.reasoning_effort = 'high' } if (options.passThinkingToggle) { result.thinking = { type: body.thinking.type } } } return result } function convertMessage( msg: AnthropicMessage, output: OpenAIChatMessage[], options: { roundTripReasoningContent?: boolean }, ): void { const content = msg.content // Simple string content if (typeof content === 'string') { output.push({ role: msg.role, content }) return } // Array content blocks if (!Array.isArray(content) || content.length === 0) { output.push({ role: msg.role, content: '' }) return } if (msg.role === 'user') { convertUserMessage(content, output) } else { convertAssistantMessage(content, output, options) } } function convertUserMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[]): void { // Separate tool_result blocks from other content const contentParts: OpenAIChatContentPart[] = [] for (const block of blocks) { if (block.type === 'text') { contentParts.push({ type: 'text', text: block.text }) } else if (block.type === 'image') { const url = `data:${block.source.media_type};base64,${block.source.data}` contentParts.push({ type: 'image_url', image_url: { url } }) } else if (block.type === 'tool_result') { // tool_result → separate tool message const resultContent = typeof block.content === 'string' ? block.content : Array.isArray(block.content) ? block.content.filter((b): b is Extract => b.type === 'text').map((b) => b.text).join('\n') : '' output.push({ role: 'tool', tool_call_id: block.tool_use_id, content: resultContent, }) } } if (contentParts.length > 0) { output.push({ role: 'user', content: contentParts.length === 1 && contentParts[0].type === 'text' ? contentParts[0].text : contentParts, }) } } function convertAssistantMessage( blocks: AnthropicContentBlock[], output: OpenAIChatMessage[], options: { roundTripReasoningContent?: boolean }, ): void { let textContent = '' let reasoningContent = '' const toolCalls: OpenAIToolCall[] = [] for (const block of blocks) { if (block.type === 'text') { textContent += block.text } else if (block.type === 'thinking' && options.roundTripReasoningContent) { reasoningContent += block.thinking } else if (block.type === 'tool_use') { toolCalls.push({ id: block.id, type: 'function', function: { name: block.name, arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input), }, }) } } const msg: OpenAIChatMessage = { role: 'assistant', content: textContent || null, } if (toolCalls.length > 0) { msg.tool_calls = toolCalls } if (reasoningContent) { msg.reasoning_content = reasoningContent } output.push(msg) } function convertToolChoice(choice: unknown): unknown { if (typeof choice === 'string') return choice if (typeof choice === 'object' && choice !== null) { const c = choice as Record if (c.type === 'auto') return 'auto' if (c.type === 'any') return 'required' if (c.type === 'none') return 'none' if (c.type === 'tool' && typeof c.name === 'string') { return { type: 'function', function: { name: c.name } } } } return 'auto' }