File size: 2,636 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
/**
 * 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'
  }
}