codev / src /server /proxy /transform /openaiResponsesToAnthropic.ts
chenbhao's picture
feat: desktop
1f21206
Raw
History Blame Contribute Delete
2.64 kB
/**
* Response transformation: OpenAI Responses API → Anthropic Messages
* Derived from cc-switch (https://github.com/farion1231/cc-switch)
* Original work by Jason Young, MIT License
*/
import type {
OpenAIResponsesResponse,
OpenAIResponsesOutputItem,
AnthropicResponse,
AnthropicContentBlock,
} from './types.js'
import { parseOpenAIToolArguments } from './toolArguments.js'
/**
* Convert OpenAI Responses API response to Anthropic Messages response.
*/
export function openaiResponsesToAnthropic(response: OpenAIResponsesResponse, model: string): AnthropicResponse {
const content: AnthropicContentBlock[] = []
let hasToolUse = false
for (const item of response.output || []) {
convertOutputItem(item, content)
if (item.type === 'function_call') hasToolUse = true
}
// If no content, add empty text
if (content.length === 0) {
content.push({ type: 'text', text: '' })
}
return {
id: response.id || `msg_${Date.now()}`,
type: 'message',
role: 'assistant',
content,
model: response.model || model,
stop_reason: mapStatus(response.status, hasToolUse),
stop_sequence: null,
usage: {
input_tokens: response.usage?.input_tokens ?? response.usage?.prompt_tokens ?? 0,
output_tokens: response.usage?.output_tokens ?? response.usage?.completion_tokens ?? 0,
},
}
}
function convertOutputItem(item: OpenAIResponsesOutputItem, content: AnthropicContentBlock[]): void {
switch (item.type) {
case 'message': {
for (const part of item.content || []) {
if (part.type === 'output_text' || part.type === 'text') {
content.push({ type: 'text', text: part.text || '' })
} else if (part.type === 'refusal') {
content.push({ type: 'text', text: part.refusal || '[Refusal]' })
}
}
break
}
case 'function_call': {
content.push({
type: 'tool_use',
id: item.call_id,
name: item.name,
input: parseOpenAIToolArguments(item.arguments),
})
break
}
case 'reasoning': {
if (item.summary) {
for (const s of item.summary) {
if (s.text) {
content.push({
type: 'thinking',
thinking: s.text,
})
}
}
}
break
}
}
}
function mapStatus(status: string, hasToolUse: boolean): string {
switch (status) {
case 'completed': return hasToolUse ? 'tool_use' : 'end_turn'
case 'failed': return 'end_turn'
case 'cancelled': return 'end_turn'
case 'incomplete': return 'max_tokens'
default: return 'end_turn'
}
}