cc / src /services /openaiToClaude.js
hequ's picture
Upload 220 files
497686c verified
/**
* OpenAI 到 Claude 格式转换服务
* 处理 OpenAI API 格式与 Claude API 格式之间的转换
*/
const logger = require('../utils/logger')
class OpenAIToClaudeConverter {
constructor() {
// 停止原因映射
this.stopReasonMapping = {
end_turn: 'stop',
max_tokens: 'length',
stop_sequence: 'stop',
tool_use: 'tool_calls'
}
}
/**
* 将 OpenAI 请求格式转换为 Claude 格式
* @param {Object} openaiRequest - OpenAI 格式的请求
* @returns {Object} Claude 格式的请求
*/
convertRequest(openaiRequest) {
const claudeRequest = {
model: openaiRequest.model, // 直接使用提供的模型名,不进行映射
messages: this._convertMessages(openaiRequest.messages),
max_tokens: openaiRequest.max_tokens || 4096,
temperature: openaiRequest.temperature,
top_p: openaiRequest.top_p,
stream: openaiRequest.stream || false
}
// 定义 Claude Code 的默认系统提示词
const claudeCodeSystemMessage = "You are Claude Code, Anthropic's official CLI for Claude."
// 如果 OpenAI 请求中包含系统消息,提取并检查
const systemMessage = this._extractSystemMessage(openaiRequest.messages)
if (systemMessage && systemMessage.includes('You are currently in Xcode')) {
// Xcode 系统提示词
claudeRequest.system = systemMessage
logger.info(
`🔍 Xcode request detected, using Xcode system prompt (${systemMessage.length} chars)`
)
logger.debug(`📋 System prompt preview: ${systemMessage.substring(0, 150)}...`)
} else {
// 使用 Claude Code 默认系统提示词
claudeRequest.system = claudeCodeSystemMessage
logger.debug(
`📋 Using Claude Code default system prompt${systemMessage ? ' (ignored custom prompt)' : ''}`
)
}
// 处理停止序列
if (openaiRequest.stop) {
claudeRequest.stop_sequences = Array.isArray(openaiRequest.stop)
? openaiRequest.stop
: [openaiRequest.stop]
}
// 处理工具调用
if (openaiRequest.tools) {
claudeRequest.tools = this._convertTools(openaiRequest.tools)
if (openaiRequest.tool_choice) {
claudeRequest.tool_choice = this._convertToolChoice(openaiRequest.tool_choice)
}
}
// OpenAI 特有的参数已在转换过程中被忽略
// 包括: n, presence_penalty, frequency_penalty, logit_bias, user
logger.debug('📝 Converted OpenAI request to Claude format:', {
model: claudeRequest.model,
messageCount: claudeRequest.messages.length,
hasSystem: !!claudeRequest.system,
stream: claudeRequest.stream
})
return claudeRequest
}
/**
* 将 Claude 响应格式转换为 OpenAI 格式
* @param {Object} claudeResponse - Claude 格式的响应
* @param {String} requestModel - 原始请求的模型名
* @returns {Object} OpenAI 格式的响应
*/
convertResponse(claudeResponse, requestModel) {
const timestamp = Math.floor(Date.now() / 1000)
const openaiResponse = {
id: `chatcmpl-${this._generateId()}`,
object: 'chat.completion',
created: timestamp,
model: requestModel || 'gpt-4',
choices: [
{
index: 0,
message: this._convertClaudeMessage(claudeResponse),
finish_reason: this._mapStopReason(claudeResponse.stop_reason)
}
],
usage: this._convertUsage(claudeResponse.usage)
}
logger.debug('📝 Converted Claude response to OpenAI format:', {
responseId: openaiResponse.id,
finishReason: openaiResponse.choices[0].finish_reason,
usage: openaiResponse.usage
})
return openaiResponse
}
/**
* 转换流式响应的单个数据块
* @param {String} chunk - Claude SSE 数据块
* @param {String} requestModel - 原始请求的模型名
* @param {String} sessionId - 会话ID
* @returns {String} OpenAI 格式的 SSE 数据块
*/
convertStreamChunk(chunk, requestModel, sessionId) {
if (!chunk || chunk.trim() === '') {
return ''
}
// 解析 SSE 数据
const lines = chunk.split('\n')
const convertedChunks = []
let hasMessageStop = false
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6)
if (data === '[DONE]') {
convertedChunks.push('data: [DONE]\n\n')
continue
}
try {
const claudeEvent = JSON.parse(data)
// 检查是否是 message_stop 事件
if (claudeEvent.type === 'message_stop') {
hasMessageStop = true
}
const openaiChunk = this._convertStreamEvent(claudeEvent, requestModel, sessionId)
if (openaiChunk) {
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`)
}
} catch (e) {
// 跳过无法解析的数据,不传递非JSON格式的行
continue
}
}
// 忽略 event: 行和空行,OpenAI 格式不包含这些
}
// 如果收到 message_stop 事件,添加 [DONE] 标记
if (hasMessageStop) {
convertedChunks.push('data: [DONE]\n\n')
}
return convertedChunks.join('')
}
/**
* 提取系统消息
*/
_extractSystemMessage(messages) {
const systemMessages = messages.filter((msg) => msg.role === 'system')
if (systemMessages.length === 0) {
return null
}
// 合并所有系统消息
return systemMessages.map((msg) => msg.content).join('\n\n')
}
/**
* 转换消息格式
*/
_convertMessages(messages) {
const claudeMessages = []
for (const msg of messages) {
// 跳过系统消息(已经在 system 字段处理)
if (msg.role === 'system') {
continue
}
// 转换角色名称
const role = msg.role === 'user' ? 'user' : 'assistant'
// 转换消息内容
const { content: rawContent } = msg
let content
if (typeof rawContent === 'string') {
content = rawContent
} else if (Array.isArray(rawContent)) {
// 处理多模态内容
content = this._convertMultimodalContent(rawContent)
} else {
content = JSON.stringify(rawContent)
}
const claudeMsg = {
role,
content
}
// 处理工具调用
if (msg.tool_calls) {
claudeMsg.content = this._convertToolCalls(msg.tool_calls)
}
// 处理工具响应
if (msg.role === 'tool') {
claudeMsg.role = 'user'
claudeMsg.content = [
{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: msg.content
}
]
}
claudeMessages.push(claudeMsg)
}
return claudeMessages
}
/**
* 转换多模态内容
*/
_convertMultimodalContent(content) {
return content.map((item) => {
if (item.type === 'text') {
return {
type: 'text',
text: item.text
}
} else if (item.type === 'image_url') {
const imageUrl = item.image_url.url
// 检查是否是 base64 格式的图片
if (imageUrl.startsWith('data:')) {
// 解析 data URL: data:image/jpeg;base64,/9j/4AAQ...
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/)
if (matches) {
const mediaType = matches[1] // e.g., 'image/jpeg', 'image/png'
const base64Data = matches[2]
return {
type: 'image',
source: {
type: 'base64',
media_type: mediaType,
data: base64Data
}
}
} else {
// 如果格式不正确,尝试使用默认处理
logger.warn('⚠️ Invalid base64 image format, using default parsing')
return {
type: 'image',
source: {
type: 'base64',
media_type: 'image/jpeg',
data: imageUrl.split(',')[1] || ''
}
}
}
} else {
// 如果是 URL 格式的图片,Claude 不支持直接 URL,需要报错
logger.error(
'❌ URL images are not supported by Claude API, only base64 format is accepted'
)
throw new Error(
'Claude API only supports base64 encoded images, not URLs. Please convert the image to base64 format.'
)
}
}
return item
})
}
/**
* 转换工具定义
*/
_convertTools(tools) {
return tools.map((tool) => {
if (tool.type === 'function') {
return {
name: tool.function.name,
description: tool.function.description,
input_schema: tool.function.parameters
}
}
return tool
})
}
/**
* 转换工具选择
*/
_convertToolChoice(toolChoice) {
if (toolChoice === 'none') {
return { type: 'none' }
}
if (toolChoice === 'auto') {
return { type: 'auto' }
}
if (toolChoice === 'required') {
return { type: 'any' }
}
if (toolChoice.type === 'function') {
return {
type: 'tool',
name: toolChoice.function.name
}
}
return { type: 'auto' }
}
/**
* 转换工具调用
*/
_convertToolCalls(toolCalls) {
return toolCalls.map((tc) => ({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: JSON.parse(tc.function.arguments)
}))
}
/**
* 转换 Claude 消息为 OpenAI 格式
*/
_convertClaudeMessage(claudeResponse) {
const message = {
role: 'assistant',
content: null
}
// 处理内容
if (claudeResponse.content) {
if (typeof claudeResponse.content === 'string') {
message.content = claudeResponse.content
} else if (Array.isArray(claudeResponse.content)) {
// 提取文本内容和工具调用
const textParts = []
const toolCalls = []
for (const item of claudeResponse.content) {
if (item.type === 'text') {
textParts.push(item.text)
} else if (item.type === 'tool_use') {
toolCalls.push({
id: item.id,
type: 'function',
function: {
name: item.name,
arguments: JSON.stringify(item.input)
}
})
}
}
message.content = textParts.join('') || null
if (toolCalls.length > 0) {
message.tool_calls = toolCalls
}
}
}
return message
}
/**
* 转换停止原因
*/
_mapStopReason(claudeReason) {
return this.stopReasonMapping[claudeReason] || 'stop'
}
/**
* 转换使用统计
*/
_convertUsage(claudeUsage) {
if (!claudeUsage) {
return undefined
}
return {
prompt_tokens: claudeUsage.input_tokens || 0,
completion_tokens: claudeUsage.output_tokens || 0,
total_tokens: (claudeUsage.input_tokens || 0) + (claudeUsage.output_tokens || 0)
}
}
/**
* 转换流式事件
*/
_convertStreamEvent(event, requestModel, sessionId) {
const timestamp = Math.floor(Date.now() / 1000)
const baseChunk = {
id: sessionId,
object: 'chat.completion.chunk',
created: timestamp,
model: requestModel || 'gpt-4',
choices: [
{
index: 0,
delta: {},
finish_reason: null
}
]
}
// 根据事件类型处理
if (event.type === 'message_start') {
// 处理消息开始事件,发送角色信息
baseChunk.choices[0].delta.role = 'assistant'
return baseChunk
} else if (event.type === 'content_block_start' && event.content_block) {
if (event.content_block.type === 'text') {
baseChunk.choices[0].delta.content = event.content_block.text || ''
} else if (event.content_block.type === 'tool_use') {
// 开始工具调用
baseChunk.choices[0].delta.tool_calls = [
{
index: event.index || 0,
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: ''
}
}
]
}
} else if (event.type === 'content_block_delta' && event.delta) {
if (event.delta.type === 'text_delta') {
baseChunk.choices[0].delta.content = event.delta.text || ''
} else if (event.delta.type === 'input_json_delta') {
// 工具调用参数的增量更新
baseChunk.choices[0].delta.tool_calls = [
{
index: event.index || 0,
function: {
arguments: event.delta.partial_json || ''
}
}
]
}
} else if (event.type === 'message_delta' && event.delta) {
if (event.delta.stop_reason) {
baseChunk.choices[0].finish_reason = this._mapStopReason(event.delta.stop_reason)
}
if (event.usage) {
baseChunk.usage = this._convertUsage(event.usage)
}
} else if (event.type === 'message_stop') {
// message_stop 事件不需要返回 chunk,[DONE] 标记会在 convertStreamChunk 中添加
return null
} else {
// 忽略其他类型的事件
return null
}
return baseChunk
}
/**
* 生成随机 ID
*/
_generateId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
}
}
module.exports = new OpenAIToClaudeConverter()