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({
|
content.push({
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: message.tool_call_id,
|
tool_use_id: message.tool_call_id || message.tool_use_id,
|
||||||
content: toolContent
|
content: toolContent
|
||||||
});
|
});
|
||||||
claudeMessages.push({ role: 'user', content: content });
|
claudeMessages.push({ role: 'user', content: content });
|
||||||
|
|
@ -219,6 +219,33 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
|
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
|
||||||
}
|
}
|
||||||
break;
|
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) {
|
if (openaiRequest.tools?.length) {
|
||||||
claudeRequest.tools = openaiRequest.tools.map(t => ({
|
claudeRequest.tools = openaiRequest.tools
|
||||||
name: t.function.name,
|
.filter(t => t && ((t.function && t.function.name) || t.name))
|
||||||
description: t.function.description || '',
|
.map(t => {
|
||||||
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
if (t.function) {
|
||||||
}));
|
return {
|
||||||
claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
|
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.
|
// 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' };
|
const mapping = { auto: 'auto', none: 'none', required: 'any' };
|
||||||
return { type: mapping[toolChoice] };
|
return { type: mapping[toolChoice] };
|
||||||
}
|
}
|
||||||
if (typeof toolChoice === 'object' && toolChoice.function) {
|
if (typeof toolChoice === 'object') {
|
||||||
return { type: 'tool', name: toolChoice.function.name };
|
// 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;
|
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 映射
|
// 构建 tool_call_id -> response 映射
|
||||||
|
|
@ -633,6 +688,14 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
if (message.role === 'tool' && message.tool_call_id) {
|
if (message.role === 'tool' && message.tool_call_id) {
|
||||||
toolResponses[message.tool_call_id] = message.content;
|
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 = [];
|
const processedMessages = [];
|
||||||
|
|
@ -643,7 +706,7 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
const role = message.role;
|
const role = message.role;
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
|
|
||||||
if (role === 'system') {
|
if (role === 'system' || role === 'developer') {
|
||||||
// system -> system_instruction
|
// system -> system_instruction
|
||||||
if (messages.length > 1) {
|
if (messages.length > 1) {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
|
|
@ -790,6 +853,7 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
const node = { role: 'model', parts: [] };
|
const node = { role: 'model', parts: [] };
|
||||||
|
|
||||||
// 处理文本内容
|
// 处理文本内容
|
||||||
|
const functionCallIds = [];
|
||||||
if (typeof content === 'string' && content) {
|
if (typeof content === 'string' && content) {
|
||||||
node.parts.push({ text: content });
|
node.parts.push({ text: content });
|
||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
|
|
@ -797,6 +861,19 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
if (item.type === 'text' && item.text) {
|
if (item.type === 'text' && item.text) {
|
||||||
node.parts.push({ 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) {
|
} else if (item.type === 'image_url' && item.image_url) {
|
||||||
const imageUrl = typeof item.image_url === 'string'
|
const imageUrl = typeof item.image_url === 'string'
|
||||||
? item.image_url
|
? item.image_url
|
||||||
|
|
@ -823,9 +900,8 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 tool_calls -> functionCall
|
// 处理 OpenAI 格式 tool_calls -> functionCall
|
||||||
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
||||||
const functionCallIds = [];
|
|
||||||
for (const tc of message.tool_calls) {
|
for (const tc of message.tool_calls) {
|
||||||
if (tc.type !== 'function') continue;
|
if (tc.type !== 'function') continue;
|
||||||
const fid = tc.id || '';
|
const fid = tc.id || '';
|
||||||
|
|
@ -851,45 +927,67 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
functionCallIds.push(fid);
|
functionCallIds.push(fid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加 model 消息
|
// 添加 model 消息
|
||||||
if (node.parts.length > 0) {
|
if (node.parts.length > 0) {
|
||||||
processedMessages.push(node);
|
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 消息)
|
toolNode.parts.push({
|
||||||
if (functionCallIds.length > 0) {
|
functionResponse: {
|
||||||
const toolNode = { role: 'user', parts: [] };
|
name: functionName,
|
||||||
for (const fid of functionCallIds) {
|
response: {
|
||||||
const name = tcID2Name[fid];
|
result: responseContent
|
||||||
if (name) {
|
|
||||||
let resp = toolResponses[fid] || '{}';
|
|
||||||
// 确保 resp 是字符串
|
|
||||||
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);
|
|
||||||
}
|
if (toolNode.parts.length > 0) {
|
||||||
}
|
processedMessages.push(toolNode);
|
||||||
} else {
|
|
||||||
// 没有 tool_calls,直接添加
|
|
||||||
if (node.parts.length > 0) {
|
|
||||||
processedMessages.push(node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// tool 消息已经在 assistant 的 tool_calls 处理中合并了,这里跳过
|
// 其他 role 类型跳过
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 Gemini 请求
|
// 构建 Gemini 请求
|
||||||
|
|
@ -1025,6 +1123,12 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
functionDeclarations.push(fnDecl);
|
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 工具
|
// 处理 google_search 工具
|
||||||
|
|
@ -1334,6 +1438,8 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue