Merge pull request #369 from simonsmh/main
Fix: OpenAI tool conversion issues
This commit is contained in:
commit
4a1a382dc2
1 changed files with 155 additions and 49 deletions
|
|
@ -161,7 +161,7 @@ export class OpenAIConverter extends BaseConverter {
|
|||
}
|
||||
content.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: message.tool_call_id,
|
||||
tool_use_id: message.tool_call_id || message.tool_use_id,
|
||||
content: toolContent
|
||||
});
|
||||
claudeMessages.push({ role: 'user', content: content });
|
||||
|
|
@ -219,6 +219,33 @@ export class OpenAIConverter extends BaseConverter {
|
|||
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
|
||||
}
|
||||
break;
|
||||
case 'input_audio':
|
||||
// OpenAI 官方 input_audio 格式
|
||||
if (item.input_audio) {
|
||||
// Claude 不直接支持音频输入,转换为文本描述
|
||||
content.push({ type: 'text', text: `[Audio Input: ${item.input_audio.format || 'audio'}]` });
|
||||
}
|
||||
break;
|
||||
case 'tool_use':
|
||||
content.push({
|
||||
type: 'tool_use',
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
input: typeof item.input === 'string' ? safeParseJSON(item.input) : (item.input || {})
|
||||
});
|
||||
break;
|
||||
case 'tool_result': {
|
||||
let resultContent = item.content;
|
||||
if (typeof resultContent === 'object' && resultContent !== null) {
|
||||
resultContent = JSON.stringify(resultContent);
|
||||
}
|
||||
content.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: item.tool_use_id || item.id,
|
||||
content: resultContent
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -276,12 +303,25 @@ export class OpenAIConverter extends BaseConverter {
|
|||
}
|
||||
|
||||
if (openaiRequest.tools?.length) {
|
||||
claudeRequest.tools = openaiRequest.tools.map(t => ({
|
||||
name: t.function.name,
|
||||
description: t.function.description || '',
|
||||
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
||||
}));
|
||||
claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
|
||||
claudeRequest.tools = openaiRequest.tools
|
||||
.filter(t => t && ((t.function && t.function.name) || t.name))
|
||||
.map(t => {
|
||||
if (t.function) {
|
||||
return {
|
||||
name: t.function.name,
|
||||
description: t.function.description || '',
|
||||
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: t.name,
|
||||
description: t.description || '',
|
||||
input_schema: t.input_schema || { type: 'object', properties: {} }
|
||||
};
|
||||
});
|
||||
if (claudeRequest.tools.length > 0) {
|
||||
claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional passthrough: request-side "thinking" controls for Claude/Kiro.
|
||||
|
|
@ -596,8 +636,15 @@ export class OpenAIConverter extends BaseConverter {
|
|||
const mapping = { auto: 'auto', none: 'none', required: 'any' };
|
||||
return { type: mapping[toolChoice] };
|
||||
}
|
||||
if (typeof toolChoice === 'object' && toolChoice.function) {
|
||||
return { type: 'tool', name: toolChoice.function.name };
|
||||
if (typeof toolChoice === 'object') {
|
||||
// Claude 原生格式:{ type, name }
|
||||
if (toolChoice.type && toolChoice.name) {
|
||||
return { type: toolChoice.type, name: toolChoice.name };
|
||||
}
|
||||
// OpenAI 格式:{ function: { name } }
|
||||
if (toolChoice.function) {
|
||||
return { type: 'tool', name: toolChoice.function.name };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -625,6 +672,14 @@ export class OpenAIConverter extends BaseConverter {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Claude 格式:content 数组中的 tool_use
|
||||
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
||||
for (const item of message.content) {
|
||||
if (item && item.type === 'tool_use' && item.id && item.name) {
|
||||
tcID2Name[item.id] = item.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 tool_call_id -> response 映射
|
||||
|
|
@ -633,6 +688,14 @@ export class OpenAIConverter extends BaseConverter {
|
|||
if (message.role === 'tool' && message.tool_call_id) {
|
||||
toolResponses[message.tool_call_id] = message.content;
|
||||
}
|
||||
// Claude 格式:user content 数组中的 tool_result
|
||||
if (message.role === 'user' && Array.isArray(message.content)) {
|
||||
for (const item of message.content) {
|
||||
if (item && item.type === 'tool_result' && item.tool_use_id) {
|
||||
toolResponses[item.tool_use_id] = item.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessages = [];
|
||||
|
|
@ -643,7 +706,7 @@ export class OpenAIConverter extends BaseConverter {
|
|||
const role = message.role;
|
||||
const content = message.content;
|
||||
|
||||
if (role === 'system') {
|
||||
if (role === 'system' || role === 'developer') {
|
||||
// system -> system_instruction
|
||||
if (messages.length > 1) {
|
||||
if (typeof content === 'string') {
|
||||
|
|
@ -790,6 +853,7 @@ export class OpenAIConverter extends BaseConverter {
|
|||
const node = { role: 'model', parts: [] };
|
||||
|
||||
// 处理文本内容
|
||||
const functionCallIds = [];
|
||||
if (typeof content === 'string' && content) {
|
||||
node.parts.push({ text: content });
|
||||
} else if (Array.isArray(content)) {
|
||||
|
|
@ -797,6 +861,19 @@ export class OpenAIConverter extends BaseConverter {
|
|||
if (!item) continue;
|
||||
if (item.type === 'text' && item.text) {
|
||||
node.parts.push({ text: item.text });
|
||||
} else if (item.type === 'tool_use') {
|
||||
// Claude 格式 tool_use -> Gemini functionCall
|
||||
const fid = item.id || '';
|
||||
const fname = item.name || '';
|
||||
const argsObj = typeof item.input === 'string' ? (() => { try { return JSON.parse(item.input); } catch(e) { return {}; } })() : (item.input || {});
|
||||
node.parts.push({
|
||||
functionCall: {
|
||||
name: fname,
|
||||
args: argsObj
|
||||
},
|
||||
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
||||
});
|
||||
if (fid) functionCallIds.push(fid);
|
||||
} else if (item.type === 'image_url' && item.image_url) {
|
||||
const imageUrl = typeof item.image_url === 'string'
|
||||
? item.image_url
|
||||
|
|
@ -823,22 +900,21 @@ export class OpenAIConverter extends BaseConverter {
|
|||
}
|
||||
}
|
||||
|
||||
// 处理 tool_calls -> functionCall
|
||||
// 处理 OpenAI 格式 tool_calls -> functionCall
|
||||
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
||||
const functionCallIds = [];
|
||||
for (const tc of message.tool_calls) {
|
||||
if (tc.type !== 'function') continue;
|
||||
const fid = tc.id || '';
|
||||
const fname = tc.function?.name || '';
|
||||
const fargs = tc.function?.arguments || '{}';
|
||||
|
||||
|
||||
let argsObj;
|
||||
try {
|
||||
argsObj = typeof fargs === 'string' ? JSON.parse(fargs) : fargs;
|
||||
} catch (e) {
|
||||
argsObj = {};
|
||||
}
|
||||
|
||||
|
||||
node.parts.push({
|
||||
functionCall: {
|
||||
name: fname,
|
||||
|
|
@ -846,50 +922,72 @@ export class OpenAIConverter extends BaseConverter {
|
|||
},
|
||||
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
||||
});
|
||||
|
||||
|
||||
if (fid) {
|
||||
functionCallIds.push(fid);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 model 消息
|
||||
if (node.parts.length > 0) {
|
||||
processedMessages.push(node);
|
||||
}
|
||||
|
||||
// 添加 model 消息
|
||||
if (node.parts.length > 0) {
|
||||
processedMessages.push(node);
|
||||
}
|
||||
|
||||
// 添加对应的 functionResponse(作为 user 消息)
|
||||
if (functionCallIds.length > 0) {
|
||||
const toolNode = { role: 'user', parts: [] };
|
||||
for (const fid of functionCallIds) {
|
||||
const name = tcID2Name[fid];
|
||||
if (name) {
|
||||
let resp = toolResponses[fid] || '{}';
|
||||
if (typeof resp !== 'string') {
|
||||
resp = JSON.stringify(resp);
|
||||
}
|
||||
toolNode.parts.push({
|
||||
functionResponse: {
|
||||
name: name,
|
||||
response: {
|
||||
result: resp
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (toolNode.parts.length > 0) {
|
||||
processedMessages.push(toolNode);
|
||||
}
|
||||
}
|
||||
} else if (role === 'tool') {
|
||||
// 处理独立的 tool role 消息(OpenAI 格式)
|
||||
// 转换为 Gemini 的 functionResponse 格式
|
||||
const toolNode = { role: 'user', parts: [] };
|
||||
|
||||
// 从 tool_call_id 查找对应的函数名
|
||||
const toolCallId = message.tool_call_id;
|
||||
const functionName = tcID2Name[toolCallId];
|
||||
|
||||
if (functionName) {
|
||||
let responseContent = message.content;
|
||||
if (typeof responseContent !== 'string') {
|
||||
responseContent = JSON.stringify(responseContent);
|
||||
}
|
||||
|
||||
// 添加对应的 functionResponse(作为 user 消息)
|
||||
if (functionCallIds.length > 0) {
|
||||
const toolNode = { role: 'user', parts: [] };
|
||||
for (const fid of functionCallIds) {
|
||||
const name = tcID2Name[fid];
|
||||
if (name) {
|
||||
let resp = toolResponses[fid] || '{}';
|
||||
// 确保 resp 是字符串
|
||||
if (typeof resp !== 'string') {
|
||||
resp = JSON.stringify(resp);
|
||||
}
|
||||
toolNode.parts.push({
|
||||
functionResponse: {
|
||||
name: name,
|
||||
response: {
|
||||
result: resp
|
||||
}
|
||||
}
|
||||
});
|
||||
toolNode.parts.push({
|
||||
functionResponse: {
|
||||
name: functionName,
|
||||
response: {
|
||||
result: responseContent
|
||||
}
|
||||
}
|
||||
if (toolNode.parts.length > 0) {
|
||||
processedMessages.push(toolNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有 tool_calls,直接添加
|
||||
if (node.parts.length > 0) {
|
||||
processedMessages.push(node);
|
||||
});
|
||||
|
||||
if (toolNode.parts.length > 0) {
|
||||
processedMessages.push(toolNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
// tool 消息已经在 assistant 的 tool_calls 处理中合并了,这里跳过
|
||||
// 其他 role 类型跳过
|
||||
}
|
||||
|
||||
// 构建 Gemini 请求
|
||||
|
|
@ -1013,7 +1111,7 @@ export class OpenAIConverter extends BaseConverter {
|
|||
name: String(func.name || ''),
|
||||
description: String(func.description || '')
|
||||
};
|
||||
|
||||
|
||||
// 处理 parameters -> parametersJsonSchema
|
||||
if (func.parameters) {
|
||||
fnDecl.parametersJsonSchema = cleanJsonSchema(func.parameters);
|
||||
|
|
@ -1023,8 +1121,14 @@ export class OpenAIConverter extends BaseConverter {
|
|||
properties: {}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
functionDeclarations.push(fnDecl);
|
||||
} else if (t.name) {
|
||||
functionDeclarations.push({
|
||||
name: String(t.name),
|
||||
description: String(t.description || ''),
|
||||
parametersJsonSchema: cleanJsonSchema(t.input_schema || { type: 'object', properties: {} })
|
||||
});
|
||||
}
|
||||
|
||||
// 处理 google_search 工具
|
||||
|
|
@ -1334,6 +1438,8 @@ export class OpenAIConverter extends BaseConverter {
|
|||
}
|
||||
}]
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue