diff --git a/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js b/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js index b61bae3df5b..b7acd223e9d 100644 --- a/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js +++ b/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js @@ -151,12 +151,7 @@ function buildSplitToolCallId(originalId, index, command) { return `${baseId}_split_${index + 1}_${command}`; } -function getSafeCombinedExecToolCall(message) { - if (!message || typeof message !== "object") return null; - const content = message.content; - if (!Array.isArray(content) || content.length !== 1) return null; - - const toolCall = content[0]; +function getSafeExecToolCallCommand(toolCall) { if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) return null; if (toolCall.type !== "toolCall" || toolCall.name !== "exec") return null; @@ -167,12 +162,60 @@ function getSafeCombinedExecToolCall(message) { return null; } - const commands = splitSafeExecCommand(args.command); + return args.command; +} + +function getSafeCombinedExecToolCall(message) { + if (!message || typeof message !== "object") return null; + const content = message.content; + if (!Array.isArray(content) || content.length !== 1) return null; + + const toolCall = content[0]; + const command = getSafeExecToolCallCommand(toolCall); + if (command === null) return null; + + const commands = splitSafeExecCommand(command); if (!commands) return null; return { commands, toolCall }; } +function getCanonicalSafeExecToolCallSequence(message) { + if (!message || typeof message !== "object") return null; + const content = message.content; + if (!Array.isArray(content) || content.length < 2) return null; + + let combinedToolCall = null; + const expanded = []; + for (const toolCall of content) { + const command = getSafeExecToolCallCommand(toolCall); + if (command === null) return null; + + const split = splitSafeExecCommand(command); + if (split) { + combinedToolCall = combinedToolCall || toolCall; + expanded.push(...split); + } else if (SAFE_SPLIT_EXEC_COMMANDS.has(command.trim())) { + expanded.push(command.trim()); + } else { + return null; + } + } + + if (!combinedToolCall) return null; + const seen = Array.from(new Set(expanded)); + const commands = ["hostname", "date", "uptime"]; + if (seen.length !== commands.length || commands.some((command, index) => seen[index] !== command)) { + return null; + } + + return { commands, toolCall: combinedToolCall }; +} + +function getSafeExecRewrite(message) { + return getSafeCombinedExecToolCall(message) || getCanonicalSafeExecToolCallSequence(message); +} + function applySafeExecSplitToMessage(message, split) { if (!message || typeof message !== "object" || !split) return false; const { commands, toolCall } = split; @@ -188,7 +231,7 @@ function applySafeExecSplitToMessage(message, split) { } function rewriteSafeCombinedExecToolCallInMessage(message) { - return applySafeExecSplitToMessage(message, getSafeCombinedExecToolCall(message)); + return applySafeExecSplitToMessage(message, getSafeExecRewrite(message)); } function getSafeCombinedExecToolCallFromEventDelta(event) { @@ -216,8 +259,8 @@ function getSafeCombinedExecToolCallFromEventDelta(event) { function rewriteSafeCombinedExecToolCallInEvent(event) { if (!event || typeof event !== "object") return false; const split = - getSafeCombinedExecToolCall(event.partial) || - getSafeCombinedExecToolCall(event.message) || + getSafeExecRewrite(event.partial) || + getSafeExecRewrite(event.message) || getSafeCombinedExecToolCallFromEventDelta(event); if (!split) return false; diff --git a/test/kimi-inference-compat-plugin.test.ts b/test/kimi-inference-compat-plugin.test.ts index b4df0662e7d..f0a03e286fe 100644 --- a/test/kimi-inference-compat-plugin.test.ts +++ b/test/kimi-inference-compat-plugin.test.ts @@ -172,6 +172,56 @@ describe("nemoclaw Kimi inference compat plugin", () => { ]); }); + it("canonicalizes mixed streamed split calls plus the original combined call", () => { + const message = { + role: "assistant", + stopReason: "toolUse", + content: [ + { + type: "toolCall", + id: "call_kimi_exec_split_1_hostname", + name: "exec", + arguments: { command: "hostname" }, + }, + { + type: "toolCall", + id: "call_kimi_exec_split_2_date", + name: "exec", + arguments: { command: "date" }, + }, + { + type: "toolCall", + id: "call_kimi_exec", + name: "exec", + arguments: { command: "hostname; date; uptime" }, + }, + ], + }; + + expect(plugin.__testing.rewriteSafeCombinedExecToolCallInMessage(message)).toBe(true); + + expect(message.content).toEqual([ + { + type: "toolCall", + id: "call_kimi_exec_split_1_hostname", + name: "exec", + arguments: { command: "hostname" }, + }, + { + type: "toolCall", + id: "call_kimi_exec_split_2_date", + name: "exec", + arguments: { command: "date" }, + }, + { + type: "toolCall", + id: "call_kimi_exec_split_3_uptime", + name: "exec", + arguments: { command: "uptime" }, + }, + ]); + }); + it.each([ "hostname && date && uptime", "hostname; date; uptime > /tmp/out",