Skip to content
Merged
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
63 changes: 53 additions & 10 deletions nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -188,7 +231,7 @@ function applySafeExecSplitToMessage(message, split) {
}

function rewriteSafeCombinedExecToolCallInMessage(message) {
return applySafeExecSplitToMessage(message, getSafeCombinedExecToolCall(message));
return applySafeExecSplitToMessage(message, getSafeExecRewrite(message));
}

function getSafeCombinedExecToolCallFromEventDelta(event) {
Expand Down Expand Up @@ -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;

Expand Down
50 changes: 50 additions & 0 deletions test/kimi-inference-compat-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading