fix: merge consecutive tool results for Anthropic API compatibility

When the Anthropic API returns multiple parallel tool calls in a single
assistant message, all tool_result blocks must be sent together in the
next message. Previously, each tool result was sent as a separate message,
causing Anthropic's API to reject the request with:
"tool_use ids were found without tool_result blocks immediately after"

This change modifies convertFromMessages() to merge consecutive tool
messages into a single message containing all tool_result content blocks.

Fixes #375
This commit is contained in:
glod 2026-02-17 10:08:09 +01:00
parent 6244ea19fb
commit 793a07904f

View file

@ -354,7 +354,8 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] { export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
const result: ModelMessage[] = []; const result: ModelMessage[] = [];
for (const msg of messages) { for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const { providerOptions } = msg; const { providerOptions } = msg;
switch (msg.role) { switch (msg.role) {
case "assistant": case "assistant":
@ -401,11 +402,19 @@ export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelM
providerOptions, providerOptions,
}); });
break; break;
case "tool": case "tool": {
result.push({ // Collect all consecutive tool messages into a single message
role: "tool", // This is required by Anthropic's API which expects all tool_result blocks
content: [ // for parallel tool calls to be in a single message
{ const toolResults: Array<{
type: "tool-result";
toolCallId: string;
toolName: string;
output: { type: "text"; value: string };
}> = [];
// Add current tool message
toolResults.push({
type: "tool-result", type: "tool-result",
toolCallId: msg.toolCallId, toolCallId: msg.toolCallId,
toolName: msg.toolName, toolName: msg.toolName,
@ -413,13 +422,32 @@ export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelM
type: "text", type: "text",
value: msg.content, value: msg.content,
}, },
});
// Collect any consecutive tool messages
while (i + 1 < messages.length && messages[i + 1].role === "tool") {
i++;
const nextMsg = messages[i] as z.infer<typeof ToolMessage>;
toolResults.push({
type: "tool-result",
toolCallId: nextMsg.toolCallId,
toolName: nextMsg.toolName,
output: {
type: "text",
value: nextMsg.content,
}, },
], });
}
result.push({
role: "tool",
content: toolResults,
providerOptions, providerOptions,
}); });
break; break;
} }
} }
}
// doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262 // doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262
return JSON.parse(JSON.stringify(result)); return JSON.parse(JSON.stringify(result));
} }