Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 32 additions & 25 deletions src/converters/strategies/ClaudeConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ import {
* Claude转换器类
* 实现Claude协议到其他协议的转换
*/
/**
* 清洗 tool_use/tool_result ID,只保留 [a-zA-Z0-9_-] 字符
* Claude API 要求 tool_use.id 匹配 ^[a-zA-Z0-9_-]+$
*/
function sanitizeToolId(id) {
if (!id) return id;
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
}

export class ClaudeConverter extends BaseConverter {
constructor() {
super('claude');
Expand Down Expand Up @@ -948,52 +957,50 @@ export class ClaudeConverter extends BaseConverter {

case 'tool_use':
// 转换为 Gemini functionCall 格式
if (block.name && block.input) {
const args = typeof block.input === 'string'
? block.input
: JSON.stringify(block.input);
// [FIX] 允许 input 为 null/undefined/{} 等任何值,避免 tool_use 块被静默丢弃
if (block.name) {
const rawInput = block.input ?? {};
const args = typeof rawInput === 'string'
? rawInput
: JSON.stringify(rawInput);
// [FIX] 当 id 缺失时自动生成,确保 Antigravity 转 Claude 时 tool_use.id 不为空
const toolUseId = sanitizeToolId(block.id) || `toolu_${uuidv4().replace(/-/g, '')}`;

// 验证 args 是有效的 JSON 对象
try {
const parsedArgs = JSON.parse(args);
if (parsedArgs && typeof parsedArgs === 'object') {
const fc = {
name: block.name,
args: parsedArgs
args: parsedArgs,
id: toolUseId
};
// 保留 tool_use.id,Antigravity 转 Claude 时必需此字段
if (block.id) {
fc.id = block.id;
}
parts.push({
thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
functionCall: fc
});
}
} catch (e) {
// 如果解析失败,尝试直接使用 input
if (block.input && typeof block.input === 'object') {
const fc = {
name: block.name,
args: block.input
};
if (block.id) {
fc.id = block.id;
}
parts.push({
thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
functionCall: fc
});
}
const fallbackInput = rawInput && typeof rawInput === 'object' ? rawInput : {};
const fc = {
name: block.name,
args: fallbackInput,
id: toolUseId
};
parts.push({
thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
functionCall: fc
});
}
}
break;

case 'tool_result':
// 转换为 Gemini functionResponse 格式
// 的实现,正确处理 tool_use_id 到函数名的映射
const toolCallId = block.tool_use_id;
if (toolCallId) {
// [FIX] 当 tool_use_id 缺失时自动生成,确保 functionResponse.id 不为空
const toolCallId = sanitizeToolId(block.tool_use_id) || `tool_result_${uuidv4().replace(/-/g, '')}`;
{
// 尝试从之前的 tool_use 块中查找对应的函数名
// 如果找不到,则从 tool_use_id 中提取
let funcName = toolCallId;
Expand Down
95 changes: 70 additions & 25 deletions src/converters/strategies/OpenAIConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ import {
streamStateManager
} from '../../providers/openai/openai-responses-core.mjs';

/**
* 清洗 tool_use/functionCall ID,只保留 [a-zA-Z0-9_-] 字符
* Claude API 要求 tool_use.id 匹配 ^[a-zA-Z0-9_-]+$
*/
function sanitizeToolId(id) {
if (!id) return id;
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
}

/**
* OpenAI转换器类
* 实现OpenAI协议到其他协议的转换
Expand Down Expand Up @@ -674,43 +683,45 @@ export class OpenAIConverter extends BaseConverter {
const messages = openaiRequest.messages || [];
const model = openaiRequest.model || '';

// 构建 tool_call_id -> function_name 映射
// 构建 tool_call_id -> function_name 映射(ID 统一清洗,与后续查找保持一致)
const tcID2Name = {};
for (const message of messages) {
if (message.role === 'assistant' && message.tool_calls) {
for (const tc of message.tool_calls) {
if (tc.type === 'function' && tc.id && tc.function?.name) {
tcID2Name[tc.id] = tc.function.name;
tcID2Name[sanitizeToolId(tc.id)] = tc.function.name;
}
}
}
// 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;
tcID2Name[sanitizeToolId(item.id)] = item.name;
}
}
}
}

// 构建 tool_call_id -> response 映射
// 构建 tool_call_id -> response 映射(ID 统一清洗,与后续查找保持一致)
const toolResponses = {};
for (const message of messages) {
if (message.role === 'tool' && message.tool_call_id) {
toolResponses[message.tool_call_id] = message.content;
toolResponses[sanitizeToolId(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;
toolResponses[sanitizeToolId(item.tool_use_id)] = item.content;
}
}
}
}

const processedMessages = [];
// [FIX] 跟踪已生成的 functionResponse ID,防止重复(Claude API 要求每个 tool_use 只有一个 tool_result)
const emittedToolResultIds = new Set();
let systemInstruction = null;

for (let i = 0; i < messages.length; i++) {
Expand Down Expand Up @@ -854,6 +865,33 @@ export class OpenAIConverter extends BaseConverter {
}
}
break;
// [FIX] 处理嵌入在 user 消息中的 Claude 格式 tool_result 块(去重)
case 'tool_result': {
const trId = sanitizeToolId(item.tool_use_id) || `tool_result_${uuidv4().replace(/-/g, '')}`;
if (!emittedToolResultIds.has(trId)) {
emittedToolResultIds.add(trId);
const trName = tcID2Name[trId] || trId;
let trContent = item.content;
if (Array.isArray(trContent)) {
trContent = trContent
.filter(c => c && c.type === 'text')
.map(c => c.text)
.join('\n') || JSON.stringify(trContent);
} else if (typeof trContent !== 'string') {
trContent = JSON.stringify(trContent);
}
node.parts.push({
functionResponse: {
name: trName,
id: trId,
response: {
result: trContent
}
}
});
}
break;
}
}
}
}
Expand All @@ -875,17 +913,20 @@ export class OpenAIConverter extends BaseConverter {
node.parts.push({ text: item.text });
} else if (item.type === 'tool_use') {
// Claude 格式 tool_use -> Gemini functionCall
const fid = item.id || '';
// [FIX] 当 id 缺失时自动生成,确保 Antigravity 转 Claude 时 tool_use.id 不为空
const fid = sanitizeToolId(item.id) || `toolu_${uuidv4().replace(/-/g, '')}`;
const fname = item.name || '';
const argsObj = typeof item.input === 'string' ? (() => { try { return JSON.parse(item.input); } catch(e) { return {}; } })() : (item.input || {});
const fc = {
name: fname,
args: argsObj,
id: fid
};
node.parts.push({
functionCall: {
name: fname,
args: argsObj
},
functionCall: fc,
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
});
if (fid) functionCallIds.push(fid);
functionCallIds.push(fid);
} else if (item.type === 'image_url' && item.image_url) {
const imageUrl = typeof item.image_url === 'string'
? item.image_url
Expand Down Expand Up @@ -916,7 +957,8 @@ export class OpenAIConverter extends BaseConverter {
if (message.tool_calls && Array.isArray(message.tool_calls)) {
for (const tc of message.tool_calls) {
if (tc.type !== 'function') continue;
const fid = tc.id || '';
// [FIX] 当 id 缺失时自动生成,确保 Antigravity 转 Claude 时 tool_use.id 不为空
const fid = sanitizeToolId(tc.id) || `call_${uuidv4().replace(/-/g, '')}`;
const fname = tc.function?.name || '';
const fargs = tc.function?.arguments || '{}';

Expand All @@ -930,14 +972,13 @@ export class OpenAIConverter extends BaseConverter {
node.parts.push({
functionCall: {
name: fname,
args: argsObj
args: argsObj,
id: fid
},
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
});

if (fid) {
functionCallIds.push(fid);
}
functionCallIds.push(fid);
}
}

Expand All @@ -946,10 +987,11 @@ export class OpenAIConverter extends BaseConverter {
processedMessages.push(node);
}

// 添加对应的 functionResponse(作为 user 消息)
// 添加对应的 functionResponse(作为 user 消息,去重
if (functionCallIds.length > 0) {
const toolNode = { role: 'user', parts: [] };
for (const fid of functionCallIds) {
if (emittedToolResultIds.has(fid)) continue; // [FIX] 去重
const name = tcID2Name[fid];
if (name) {
let resp = toolResponses[fid] || '{}';
Expand All @@ -959,11 +1001,13 @@ export class OpenAIConverter extends BaseConverter {
toolNode.parts.push({
functionResponse: {
name: name,
id: fid,
response: {
result: resp
}
}
});
emittedToolResultIds.add(fid);
}
}
if (toolNode.parts.length > 0) {
Expand All @@ -973,13 +1017,13 @@ export class OpenAIConverter extends BaseConverter {
} 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) {
// [FIX] 去重:跳过已由 assistant 自动配对生成的 functionResponse
const toolCallId = sanitizeToolId(message.tool_call_id) || `tool_result_${uuidv4().replace(/-/g, '')}`;
if (!emittedToolResultIds.has(toolCallId)) {
emittedToolResultIds.add(toolCallId);
const toolNode = { role: 'user', parts: [] };
const functionName = tcID2Name[toolCallId] || toolCallId;
let responseContent = message.content;
if (typeof responseContent !== 'string') {
responseContent = JSON.stringify(responseContent);
Expand All @@ -988,6 +1032,7 @@ export class OpenAIConverter extends BaseConverter {
toolNode.parts.push({
functionResponse: {
name: functionName,
id: toolCallId,
response: {
result: responseContent
}
Expand Down