fix(combo/fusion): flatten Anthropic-style tool messages in panel calls

flattenToolHistory only recognized OpenAI-style tool calls. Anthropic-compatible clients (Claude Code, /v1/messages) send tool invocations and results as tool_use/tool_result blocks inside the message content array. Since panel calls strip the tools definitions (#1859), the panel expert models received structured tool history without schemas, causing them to fail or misbehave (leading to empty responses and 503 errors).

Extend flattenToolHistory to recognize and flatten tool_use and tool_result blocks in the content array into prose text, keeping panel expert execution robust and independent of the client API format.

PR decolua/9router#1910

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
warelik
2026-06-20 15:18:05 +07:00
committed by decolua
parent 707a91555d
commit 86162eeb8f
2 changed files with 54 additions and 0 deletions

View File

@@ -31,6 +31,29 @@ function flattenToolHistory(messages) {
const base = extractTextContent(rest.content) || (typeof rest.content === "string" ? rest.content : "");
return { ...rest, content: `${base}${base ? "\n" : ""}${TOOL_CALL_PREFIX}${names}]` };
}
if (Array.isArray(msg.content)) {
const hasToolUse = msg.content.some((c) => c.type === "tool_use");
const hasToolResult = msg.content.some((c) => c.type === "tool_result");
if (hasToolUse || hasToolResult) {
const textParts = [];
const toolNames = [];
const toolResults = [];
for (const block of msg.content) {
if (block.type === "text" && block.text) textParts.push(block.text);
if (block.type === "tool_use") toolNames.push(block.name || "tool");
if (block.type === "tool_result") toolResults.push(extractTextContent(block.content) || String(block.content ?? ""));
}
const { ...rest } = msg;
let newContent = textParts.join("\n");
if (toolNames.length > 0) {
newContent = `${newContent}${newContent ? "\n" : ""}${TOOL_CALL_PREFIX}${toolNames.join(", ")}]`;
}
if (toolResults.length > 0) {
newContent = `${newContent}${newContent ? "\n" : ""}${TOOL_RESULT_PREFIX}${toolResults.join("\n")}]`;
}
return { ...rest, content: newContent };
}
}
return msg;
});
}

View File

@@ -182,4 +182,35 @@ describe("fusion combo", () => {
expect(judgeBody.messages[1].tool_calls).toBeDefined();
expect(judgeBody.messages[2].role).toBe("tool");
});
it("flattens Anthropic-style tool_use and tool_result blocks in arrays", async () => {
const handleSingleModel = vi.fn(async () => okResponse("ans"));
await handleFusionChat({
body: {
messages: [
{ role: "user", content: "do it" },
{ role: "assistant", content: [{ type: "text", text: "ok" }, { type: "tool_use", id: "t1", name: "run" }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "done" }] }
],
tools: [{ name: "run", description: "d" }]
},
models: ["p/a", "p/b"],
handleSingleModel,
log,
judgeModel: "p/judge"
});
const panelCalls = handleSingleModel.mock.calls.filter(([,, isPanel]) => isPanel === true);
expect(panelCalls.length).toBe(2);
const panelBody = panelCalls[0][0];
expect(panelBody.tools).toBeUndefined();
expect(panelBody.messages.length).toBe(3);
// Flattened tool_use
expect(panelBody.messages[1].content).toBe("ok\n[Called tools: run]");
// Flattened tool_result
expect(panelBody.messages[2].content).toBe("[Tool result: done]");
});
});