File size: 5,274 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | /**
* Request transformation: Anthropic Messages → OpenAI Responses API
* Derived from cc-switch (https://github.com/farion1231/cc-switch)
* Original work by Jason Young, MIT License
*/
import type {
AnthropicRequest,
AnthropicContentBlock,
AnthropicMessage,
OpenAIResponsesRequest,
OpenAIResponsesInputItem,
OpenAIChatContentPart,
} from './types.js'
/**
* Convert Anthropic Messages request to OpenAI Responses API request.
*/
export function anthropicToOpenaiResponses(body: AnthropicRequest): OpenAIResponsesRequest {
const input: OpenAIResponsesInputItem[] = []
// Convert messages to input items
for (const msg of body.messages) {
convertMessageToInputItems(msg, input)
}
const result: OpenAIResponsesRequest = {
model: body.model,
input,
stream: body.stream,
store: false,
}
// system → instructions
if (body.system) {
if (typeof body.system === 'string') {
result.instructions = body.system
} else if (Array.isArray(body.system)) {
result.instructions = body.system.map((b) => b.text).join('\n')
}
}
// max_tokens — omit to let upstream provider use its own default/max.
// Claude Code sends very large values that exceed many providers' limits.
// temperature & top_p
if (body.temperature !== undefined) result.temperature = body.temperature
if (body.top_p !== undefined) result.top_p = body.top_p
// tools
if (body.tools && body.tools.length > 0) {
result.tools = body.tools
.filter((t) => t.name !== 'BatchTool')
.map((t) => ({
type: '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
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' }
}
}
// stop_sequences not supported in Responses API, dropped
return result
}
function convertMessageToInputItems(msg: AnthropicMessage, output: OpenAIResponsesInputItem[]): void {
const content = msg.content
// Simple string content
if (typeof content === 'string') {
output.push({ type: 'message', role: msg.role, content })
return
}
if (!Array.isArray(content) || content.length === 0) {
output.push({ type: 'message', role: msg.role, content: '' })
return
}
// Collect text/image parts and handle tool blocks separately
const contentParts: (string | OpenAIChatContentPart)[] = []
for (const block of content) {
if (block.type === 'text') {
contentParts.push(block.text)
} else if (block.type === 'image') {
contentParts.push({
type: 'image_url',
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` },
})
} else if (block.type === 'tool_use') {
// Flush any accumulated content first
if (contentParts.length > 0) {
const flatContent = contentParts.length === 1 && typeof contentParts[0] === 'string'
? contentParts[0]
: contentParts.map((p) => typeof p === 'string' ? p : '').join('')
if (flatContent) {
output.push({ type: 'message', role: msg.role, content: flatContent })
}
contentParts.length = 0
}
// Lift to function_call item
output.push({
type: 'function_call',
call_id: block.id,
name: block.name,
arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input),
})
} else if (block.type === 'tool_result') {
// Lift to function_call_output item
const resultContent = typeof block.content === 'string'
? block.content
: Array.isArray(block.content)
? block.content.filter((b): b is Extract<AnthropicContentBlock, { type: 'text' }> => b.type === 'text').map((b) => b.text).join('\n')
: ''
output.push({
type: 'function_call_output',
call_id: block.tool_use_id,
output: resultContent,
})
}
// Skip thinking blocks
}
// Flush remaining content
if (contentParts.length > 0) {
const flatContent = contentParts.length === 1 && typeof contentParts[0] === 'string'
? contentParts[0]
: contentParts.map((p) => typeof p === 'string' ? p : '').join('')
if (flatContent) {
output.push({ type: 'message', role: msg.role, content: flatContent })
}
}
}
function convertToolChoice(choice: unknown): unknown {
if (typeof choice === 'string') return choice
if (typeof choice === 'object' && choice !== null) {
const c = choice as Record<string, unknown>
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'
}
|