diff --git a/src/api/transform/__tests__/openai-format.spec.ts b/src/api/transform/__tests__/openai-format.spec.ts index 111f79101f..33996763ef 100644 --- a/src/api/transform/__tests__/openai-format.spec.ts +++ b/src/api/transform/__tests__/openai-format.spec.ts @@ -482,6 +482,122 @@ describe("convertToOpenAiMessages", () => { expect(openAiMessages).toHaveLength(1) expect(openAiMessages[0].role).toBe("user") }) + + it("should merge text into tool messages for multiple tool calls across conversation turns", () => { + // This test simulates a full conversation with multiple tool_result + environment_details messages + // to ensure mergeToolResultText works correctly for ALL tool_result messages, not just the first one + // Regression test for: "The fix works for the first message but after the first response the text content is NOT merged" + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + // Initial user message (no tool_results) + { + role: "user", + content: [ + { type: "text", text: "Create a file for me" }, + { type: "text", text: "Context 1" }, + ], + }, + // Assistant uses first tool + { + role: "assistant", + content: [ + { type: "text", text: "I'll create the file for you." }, + { + type: "tool_use", + id: "call_1", + name: "write_file", + input: { path: "test.txt", content: "hello" }, + }, + ], + }, + // First tool result + environment_details + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "call_1", content: "File created successfully" }, + { type: "text", text: "Context 2" }, + ], + }, + // Assistant uses second tool + { + role: "assistant", + content: [ + { type: "text", text: "Now I'll read the file to verify." }, + { type: "tool_use", id: "call_2", name: "read_file", input: { path: "test.txt" } }, + ], + }, + // Second tool result + environment_details (this is where the bug was reported) + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "call_2", content: "File content: hello" }, + { type: "text", text: "Context 3" }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages, { mergeToolResultText: true }) + + // Expected structure: + // 1. User message (initial, no tool_results - text should remain as user message) + // 2. Assistant message with tool_calls + // 3. Tool message with merged text (first tool_result) + // 4. Assistant message with tool_calls + // 5. Tool message with merged text (second tool_result) + expect(openAiMessages).toHaveLength(5) + + // First message should be a user message (no tool_results to merge into) + expect(openAiMessages[0].role).toBe("user") + + // Second message should be assistant with tool_calls + expect(openAiMessages[1].role).toBe("assistant") + expect((openAiMessages[1] as OpenAI.Chat.ChatCompletionAssistantMessageParam).tool_calls).toHaveLength(1) + + // Third message should be tool message with merged environment_details + const firstToolMsg = openAiMessages[2] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(firstToolMsg.role).toBe("tool") + expect(firstToolMsg.tool_call_id).toBe("call_1") + expect(firstToolMsg.content).toContain("File created successfully") + expect(firstToolMsg.content).toContain("Context 2") + + // Fourth message should be assistant with tool_calls + expect(openAiMessages[3].role).toBe("assistant") + expect((openAiMessages[3] as OpenAI.Chat.ChatCompletionAssistantMessageParam).tool_calls).toHaveLength(1) + + // Fifth message should be tool message with merged environment_details (THE BUG FIX) + const secondToolMsg = openAiMessages[4] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(secondToolMsg.role).toBe("tool") + expect(secondToolMsg.tool_call_id).toBe("call_2") + expect(secondToolMsg.content).toContain("File content: hello") + expect(secondToolMsg.content).toContain("Context 3") + }) + + it("should NOT create user messages after tool messages when mergeToolResultText is true", () => { + // This test specifically verifies that the "user after tool" error is avoided + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [{ type: "tool_use", id: "tool_1", name: "read_file", input: { path: "test.ts" } }], + }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "tool_1", content: "File contents" }, + { type: "text", text: "Some context" }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages, { mergeToolResultText: true }) + + // Should produce assistant + tool (no user message) + expect(openAiMessages).toHaveLength(2) + expect(openAiMessages[0].role).toBe("assistant") + expect(openAiMessages[1].role).toBe("tool") + // The text should be merged into the tool message, NOT as a separate user message + expect((openAiMessages[1] as OpenAI.Chat.ChatCompletionToolMessageParam).content).toContain( + "Some context", + ) + }) }) describe("reasoning_details transformation", () => {