mirror of https://github.com/openclaw/openclaw.git
fix: preserve streamed Kimi tool args on repair fallback
This commit is contained in:
parent
5bef64bc31
commit
e675634eb3
|
|
@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Providers/GitHub Copilot: send IDE identity headers on runtime model requests and GitHub token exchange so IDE-authenticated Copilot runs stop failing with missing `Editor-Version`. (#60641) Thanks @VACInc and @vincentkoc.
|
||||
- Model picker/providers: treat bundled BytePlus and Volcengine plan aliases as their native providers during setup, and expose their bundled standard/coding catalogs before auth so setup can suggest the right models. (#58819) Thanks @Luckymingxuan.
|
||||
- Prompt caching: route Codex Responses and Anthropic Vertex through boundary-aware cache shaping, and report the actual outbound system prompt in cache traces so cache reuse and misses line up with what providers really receive. Thanks @vincentkoc.
|
||||
- Agents/Kimi tool-call repair: preserve tool arguments that were already present on streamed tool calls when later malformed deltas fail reevaluation, while still dropping stale repair-only state before `toolcall_end`.
|
||||
- MiniMax/pricing: keep bundled MiniMax highspeed pricing distinct in provider catalogs and preserve the lower M2.5 cache-read pricing when onboarding older MiniMax models. (#54214) Thanks @octo-patch.
|
||||
- Agents/cache: preserve the full 3-turn prompt-cache image window across tool loops, keep colliding bundled MCP tool definitions deterministic, and reapply Anthropic Vertex cache shaping after payload hook replacements so KV/cache reuse stays stable. Thanks @vincentkoc.
|
||||
- Device pairing: reject rotating device tokens into roles that were never approved during pairing, and keep reconnect role checks bounded to the paired device's approved role set. (#60462) Thanks @eleqtrizit.
|
||||
|
|
|
|||
|
|
@ -1776,6 +1776,43 @@ describe("wrapStreamFnRepairMalformedToolCallArguments", () => {
|
|||
expect(partialToolCall.arguments).toEqual({});
|
||||
expect(streamedToolCall.arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("preserves preexisting tool arguments when later reevaluation fails", async () => {
|
||||
const partialToolCall = {
|
||||
type: "toolCall",
|
||||
name: "read",
|
||||
arguments: { path: "/etc/hosts" },
|
||||
};
|
||||
const streamedToolCall = { type: "toolCall", name: "read", arguments: {} };
|
||||
const partialMessage = { role: "assistant", content: [partialToolCall] };
|
||||
const baseFn = vi.fn(() =>
|
||||
createFakeStream({
|
||||
events: [
|
||||
{
|
||||
type: "toolcall_delta",
|
||||
contentIndex: 0,
|
||||
delta: "}",
|
||||
partial: partialMessage,
|
||||
},
|
||||
{
|
||||
type: "toolcall_end",
|
||||
contentIndex: 0,
|
||||
toolCall: streamedToolCall,
|
||||
partial: partialMessage,
|
||||
},
|
||||
],
|
||||
resultMessage: { role: "assistant", content: [partialToolCall] },
|
||||
}),
|
||||
);
|
||||
|
||||
const stream = await invokeWrappedStream(baseFn);
|
||||
for await (const _item of stream) {
|
||||
// drain
|
||||
}
|
||||
|
||||
expect(partialToolCall.arguments).toEqual({ path: "/etc/hosts" });
|
||||
expect(streamedToolCall.arguments).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOllamaCompatProvider", () => {
|
||||
|
|
|
|||
|
|
@ -171,6 +171,30 @@ function repairToolCallArgumentsInMessage(
|
|||
typedBlock.arguments = repairedArgs;
|
||||
}
|
||||
|
||||
function hasMeaningfulToolCallArgumentsInMessage(message: unknown, contentIndex: number): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) {
|
||||
return false;
|
||||
}
|
||||
const block = content[contentIndex];
|
||||
if (!block || typeof block !== "object") {
|
||||
return false;
|
||||
}
|
||||
const typedBlock = block as { type?: unknown; arguments?: unknown };
|
||||
if (!isToolCallBlockType(typedBlock.type)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typedBlock.arguments !== null &&
|
||||
typeof typedBlock.arguments === "object" &&
|
||||
!Array.isArray(typedBlock.arguments) &&
|
||||
Object.keys(typedBlock.arguments as Record<string, unknown>).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function clearToolCallArgumentsInMessage(message: unknown, contentIndex: number): void {
|
||||
if (!message || typeof message !== "object") {
|
||||
return;
|
||||
|
|
@ -211,6 +235,7 @@ function wrapStreamRepairMalformedToolCallArguments(
|
|||
): ReturnType<typeof streamSimple> {
|
||||
const partialJsonByIndex = new Map<number, string>();
|
||||
const repairedArgsByIndex = new Map<number, Record<string, unknown>>();
|
||||
const hadPreexistingArgsByIndex = new Set<number>();
|
||||
const disabledIndices = new Set<number>();
|
||||
const loggedRepairIndices = new Set<number>();
|
||||
const originalResult = stream.result.bind(stream);
|
||||
|
|
@ -219,6 +244,7 @@ function wrapStreamRepairMalformedToolCallArguments(
|
|||
repairMalformedToolCallArgumentsInMessage(message, repairedArgsByIndex);
|
||||
partialJsonByIndex.clear();
|
||||
repairedArgsByIndex.clear();
|
||||
hadPreexistingArgsByIndex.clear();
|
||||
disabledIndices.clear();
|
||||
loggedRepairIndices.clear();
|
||||
return message;
|
||||
|
|
@ -262,8 +288,16 @@ function wrapStreamRepairMalformedToolCallArguments(
|
|||
shouldAttemptMalformedToolCallRepair(nextPartialJson, event.delta) ||
|
||||
repairedArgsByIndex.has(event.contentIndex);
|
||||
if (shouldReevaluateRepair) {
|
||||
const hadRepairState = repairedArgsByIndex.has(event.contentIndex);
|
||||
const repair = tryExtractUsableToolCallArguments(nextPartialJson);
|
||||
if (repair) {
|
||||
if (
|
||||
!hadRepairState &&
|
||||
(hasMeaningfulToolCallArgumentsInMessage(event.partial, event.contentIndex) ||
|
||||
hasMeaningfulToolCallArgumentsInMessage(event.message, event.contentIndex))
|
||||
) {
|
||||
hadPreexistingArgsByIndex.add(event.contentIndex);
|
||||
}
|
||||
repairedArgsByIndex.set(event.contentIndex, repair.args);
|
||||
repairToolCallArgumentsInMessage(event.partial, event.contentIndex, repair.args);
|
||||
repairToolCallArgumentsInMessage(event.message, event.contentIndex, repair.args);
|
||||
|
|
@ -275,8 +309,20 @@ function wrapStreamRepairMalformedToolCallArguments(
|
|||
}
|
||||
} else {
|
||||
repairedArgsByIndex.delete(event.contentIndex);
|
||||
clearToolCallArgumentsInMessage(event.partial, event.contentIndex);
|
||||
clearToolCallArgumentsInMessage(event.message, event.contentIndex);
|
||||
// Keep args that were already present on the streamed message, but
|
||||
// clear repair-only state so stale repaired args do not get replayed.
|
||||
const hadPreexistingArgs =
|
||||
hadPreexistingArgsByIndex.has(event.contentIndex) ||
|
||||
(!hadRepairState &&
|
||||
(hasMeaningfulToolCallArgumentsInMessage(event.partial, event.contentIndex) ||
|
||||
hasMeaningfulToolCallArgumentsInMessage(
|
||||
event.message,
|
||||
event.contentIndex,
|
||||
)));
|
||||
if (!hadPreexistingArgs) {
|
||||
clearToolCallArgumentsInMessage(event.partial, event.contentIndex);
|
||||
clearToolCallArgumentsInMessage(event.message, event.contentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -294,6 +340,7 @@ function wrapStreamRepairMalformedToolCallArguments(
|
|||
repairToolCallArgumentsInMessage(event.message, event.contentIndex, repairedArgs);
|
||||
}
|
||||
partialJsonByIndex.delete(event.contentIndex);
|
||||
hadPreexistingArgsByIndex.delete(event.contentIndex);
|
||||
disabledIndices.delete(event.contentIndex);
|
||||
loggedRepairIndices.delete(event.contentIndex);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue