mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: handle tool_use/tool_result blocks in r1-format for DeepSeek Reasoner
This fixes issue #10101 where DeepSeek Reasoner (and other models using R1 format) would get stuck in a loop repeatedly calling update_todo_list. The root cause was that convertToR1Format did not handle tool_use and tool_result blocks. When native tools were used: - tool_use blocks in assistant messages were being ignored - tool_result blocks in user messages were being ignored This caused the model to not see its previous tool calls and their results, leading it to keep calling the same tool repeatedly. The fix adds proper handling for: - tool_use blocks: converted to OpenAI tool_calls format - tool_result blocks: converted to role: "tool" messages This ensures the model receives proper feedback about its tool calls and can proceed with the task instead of looping.
This commit is contained in:
parent
a7b192adca
commit
ea070ba645
2 changed files with 570 additions and 76 deletions
|
|
@ -179,4 +179,327 @@ describe("convertToR1Format", () => {
|
|||
|
||||
expect(convertToR1Format(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
// Tool handling tests
|
||||
describe("tool_use handling", () => {
|
||||
it("should convert tool_use blocks to tool_calls in assistant messages", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Read the file" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I'll read the file for you." },
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_123",
|
||||
name: "read_file",
|
||||
input: { path: "test.txt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toEqual({ role: "user", content: "Read the file" })
|
||||
|
||||
const assistantMsg = result[1] as OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
expect(assistantMsg.role).toBe("assistant")
|
||||
expect(assistantMsg.content).toBe("I'll read the file for you.")
|
||||
expect(assistantMsg.tool_calls).toEqual([
|
||||
{
|
||||
id: "toolu_123",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_file",
|
||||
arguments: JSON.stringify({ path: "test.txt" }),
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should convert multiple tool_use blocks to multiple tool_calls", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_1",
|
||||
name: "read_file",
|
||||
input: { path: "file1.txt" },
|
||||
},
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_2",
|
||||
name: "read_file",
|
||||
input: { path: "file2.txt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
const assistantMsg = result[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
|
||||
expect(assistantMsg.tool_calls).toHaveLength(2)
|
||||
expect(assistantMsg.tool_calls![0].id).toBe("toolu_1")
|
||||
expect(assistantMsg.tool_calls![1].id).toBe("toolu_2")
|
||||
})
|
||||
|
||||
it("should handle assistant message with only tool_use (no text)", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_123",
|
||||
name: "update_todo_list",
|
||||
input: { todos: "[ ] Task 1\n[ ] Task 2" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
const assistantMsg = result[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
|
||||
expect(assistantMsg.role).toBe("assistant")
|
||||
expect(assistantMsg.content).toBeNull()
|
||||
expect(assistantMsg.tool_calls).toHaveLength(1)
|
||||
expect((assistantMsg.tool_calls![0] as any).function.name).toBe("update_todo_list")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool_result handling", () => {
|
||||
it("should convert tool_result blocks to tool messages", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_123",
|
||||
content: "File contents here",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const toolMsg = result[0] as OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
expect(toolMsg.role).toBe("tool")
|
||||
expect(toolMsg.tool_call_id).toBe("toolu_123")
|
||||
expect(toolMsg.content).toBe("File contents here")
|
||||
})
|
||||
|
||||
it("should convert multiple tool_result blocks to multiple tool messages", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_1",
|
||||
content: "Result 1",
|
||||
},
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_2",
|
||||
content: "Result 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect((result[0] as OpenAI.Chat.ChatCompletionToolMessageParam).tool_call_id).toBe("toolu_1")
|
||||
expect((result[1] as OpenAI.Chat.ChatCompletionToolMessageParam).tool_call_id).toBe("toolu_2")
|
||||
})
|
||||
|
||||
it("should handle tool_result with array content", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_123",
|
||||
content: [
|
||||
{ type: "text", text: "Line 1" },
|
||||
{ type: "text", text: "Line 2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
const toolMsg = result[0] as OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
|
||||
expect(toolMsg.content).toBe("Line 1\nLine 2")
|
||||
})
|
||||
|
||||
it("should handle mixed tool_result and text content in user message", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_123",
|
||||
content: "Tool result",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "User feedback",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
// Tool result should come first, then user message
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].role).toBe("tool")
|
||||
expect((result[0] as OpenAI.Chat.ChatCompletionToolMessageParam).tool_call_id).toBe("toolu_123")
|
||||
expect(result[1].role).toBe("user")
|
||||
expect((result[1] as OpenAI.Chat.ChatCompletionUserMessageParam).content).toBe("User feedback")
|
||||
})
|
||||
})
|
||||
|
||||
describe("complete tool call flow", () => {
|
||||
it("should handle a complete tool call cycle (request -> tool_use -> tool_result -> response)", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Update my todo list" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I'll update your todo list." },
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_update_123",
|
||||
name: "update_todo_list",
|
||||
input: { todos: "[ ] Task 1\n[x] Task 2" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_update_123",
|
||||
content: "Todo list updated successfully.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "assistant", content: "Your todo list has been updated!" },
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
expect(result).toHaveLength(4)
|
||||
|
||||
// First user message
|
||||
expect(result[0]).toEqual({ role: "user", content: "Update my todo list" })
|
||||
|
||||
// Assistant with tool_calls
|
||||
const assistantWithTool = result[1] as OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
expect(assistantWithTool.role).toBe("assistant")
|
||||
expect(assistantWithTool.content).toBe("I'll update your todo list.")
|
||||
expect(assistantWithTool.tool_calls).toHaveLength(1)
|
||||
expect(assistantWithTool.tool_calls![0].id).toBe("toolu_update_123")
|
||||
|
||||
// Tool result
|
||||
const toolResult = result[2] as OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
expect(toolResult.role).toBe("tool")
|
||||
expect(toolResult.tool_call_id).toBe("toolu_update_123")
|
||||
expect(toolResult.content).toBe("Todo list updated successfully.")
|
||||
|
||||
// Final assistant response
|
||||
expect(result[3]).toEqual({
|
||||
role: "assistant",
|
||||
content: "Your todo list has been updated!",
|
||||
})
|
||||
})
|
||||
|
||||
it("should not merge assistant messages when tool_calls are present", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_1",
|
||||
name: "read_file",
|
||||
input: { path: "test.txt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "assistant", content: "Following up on that..." },
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
// Should have 2 separate messages, not merged
|
||||
expect(result).toHaveLength(2)
|
||||
expect((result[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam).tool_calls).toBeDefined()
|
||||
expect((result[1] as OpenAI.Chat.ChatCompletionAssistantMessageParam).tool_calls).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle multiple consecutive tool uses followed by results", () => {
|
||||
const input: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_1",
|
||||
name: "read_file",
|
||||
input: { path: "file1.txt" },
|
||||
},
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_2",
|
||||
name: "read_file",
|
||||
input: { path: "file2.txt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_1",
|
||||
content: "Content of file 1",
|
||||
},
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_2",
|
||||
content: "Content of file 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToR1Format(input)
|
||||
|
||||
expect(result).toHaveLength(3) // 1 assistant + 2 tool messages
|
||||
|
||||
const assistantMsg = result[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
expect(assistantMsg.tool_calls).toHaveLength(2)
|
||||
|
||||
expect((result[1] as OpenAI.Chat.ChatCompletionToolMessageParam).tool_call_id).toBe("toolu_1")
|
||||
expect((result[2] as OpenAI.Chat.ChatCompletionToolMessageParam).tool_call_id).toBe("toolu_2")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ type ContentPartText = OpenAI.Chat.ChatCompletionContentPartText
|
|||
type ContentPartImage = OpenAI.Chat.ChatCompletionContentPartImage
|
||||
type UserMessage = OpenAI.Chat.ChatCompletionUserMessageParam
|
||||
type AssistantMessage = OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
type ToolMessage = OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
type Message = OpenAI.Chat.ChatCompletionMessageParam
|
||||
type AnthropicMessage = Anthropic.Messages.MessageParam
|
||||
|
||||
|
|
@ -12,87 +13,257 @@ type AnthropicMessage = Anthropic.Messages.MessageParam
|
|||
* Converts Anthropic messages to OpenAI format while merging consecutive messages with the same role.
|
||||
* This is required for DeepSeek Reasoner which does not support successive messages with the same role.
|
||||
*
|
||||
* This function also handles tool_use and tool_result blocks:
|
||||
* - tool_use blocks in assistant messages are converted to tool_calls
|
||||
* - tool_result blocks in user messages are converted to role: "tool" messages
|
||||
*
|
||||
* @param messages Array of Anthropic messages
|
||||
* @returns Array of OpenAI messages where consecutive messages with the same role are combined
|
||||
*/
|
||||
export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
return messages.reduce<Message[]>((merged, message) => {
|
||||
const lastMessage = merged[merged.length - 1]
|
||||
let messageContent: string | (ContentPartText | ContentPartImage)[] = ""
|
||||
let hasImages = false
|
||||
const result: Message[] = []
|
||||
|
||||
// Convert content to appropriate format
|
||||
if (Array.isArray(message.content)) {
|
||||
const textParts: string[] = []
|
||||
const imageParts: ContentPartImage[] = []
|
||||
|
||||
message.content.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
textParts.push(part.text)
|
||||
}
|
||||
if (part.type === "image") {
|
||||
hasImages = true
|
||||
imageParts.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (hasImages) {
|
||||
const parts: (ContentPartText | ContentPartImage)[] = []
|
||||
if (textParts.length > 0) {
|
||||
parts.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
parts.push(...imageParts)
|
||||
messageContent = parts
|
||||
} else {
|
||||
messageContent = textParts.join("\n")
|
||||
}
|
||||
} else {
|
||||
messageContent = message.content
|
||||
for (const message of messages) {
|
||||
if (message.role === "user") {
|
||||
processUserMessage(message, result)
|
||||
} else if (message.role === "assistant") {
|
||||
processAssistantMessage(message, result)
|
||||
}
|
||||
}
|
||||
|
||||
// If last message has same role, merge the content
|
||||
if (lastMessage?.role === message.role) {
|
||||
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
|
||||
lastMessage.content += `\n${messageContent}`
|
||||
}
|
||||
// If either has image content, convert both to array format
|
||||
else {
|
||||
const lastContent = Array.isArray(lastMessage.content)
|
||||
? lastMessage.content
|
||||
: [{ type: "text" as const, text: lastMessage.content || "" }]
|
||||
|
||||
const newContent = Array.isArray(messageContent)
|
||||
? messageContent
|
||||
: [{ type: "text" as const, text: messageContent }]
|
||||
|
||||
if (message.role === "assistant") {
|
||||
const mergedContent = [...lastContent, ...newContent] as AssistantMessage["content"]
|
||||
lastMessage.content = mergedContent
|
||||
} else {
|
||||
const mergedContent = [...lastContent, ...newContent] as UserMessage["content"]
|
||||
lastMessage.content = mergedContent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add as new message with the correct type based on role
|
||||
if (message.role === "assistant") {
|
||||
const newMessage: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: messageContent as AssistantMessage["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
} else {
|
||||
const newMessage: UserMessage = {
|
||||
role: "user",
|
||||
content: messageContent as UserMessage["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}, [])
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a user message, handling tool_result blocks separately from text/image content
|
||||
*/
|
||||
function processUserMessage(message: AnthropicMessage, result: Message[]): void {
|
||||
if (typeof message.content === "string") {
|
||||
// Simple string content - merge with previous user message if possible
|
||||
mergeOrAddUserMessage(message.content, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Separate tool_result blocks from other content
|
||||
const toolResults: Anthropic.ToolResultBlockParam[] = []
|
||||
const otherContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_result") {
|
||||
toolResults.push(block)
|
||||
} else if (block.type === "text" || block.type === "image") {
|
||||
otherContent.push(block)
|
||||
}
|
||||
// Ignore other block types (user cannot send tool_use)
|
||||
}
|
||||
|
||||
// Process tool_result blocks first - they become separate "tool" messages
|
||||
// This must come before user content to maintain correct message order
|
||||
for (const toolResult of toolResults) {
|
||||
const content =
|
||||
typeof toolResult.content === "string"
|
||||
? toolResult.content
|
||||
: (toolResult.content?.map((part) => (part.type === "text" ? part.text : "")).join("\n") ?? "")
|
||||
|
||||
const toolMessage: ToolMessage = {
|
||||
role: "tool",
|
||||
tool_call_id: toolResult.tool_use_id,
|
||||
content: content,
|
||||
}
|
||||
result.push(toolMessage)
|
||||
}
|
||||
|
||||
// Process remaining content (text and images)
|
||||
if (otherContent.length > 0) {
|
||||
const { content, hasImages } = convertUserContent(otherContent)
|
||||
|
||||
if (hasImages) {
|
||||
// If there are images, try to merge with previous user message if possible
|
||||
mergeOrAddUserMessageWithArrayContent(content as (ContentPartText | ContentPartImage)[], result)
|
||||
} else {
|
||||
// Text only - can merge with previous user message
|
||||
const textContent =
|
||||
typeof content === "string" ? content : (content as ContentPartText[]).map((p) => p.text).join("\n")
|
||||
mergeOrAddUserMessage(textContent, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an assistant message, handling tool_use blocks as tool_calls
|
||||
*/
|
||||
function processAssistantMessage(message: AnthropicMessage, result: Message[]): void {
|
||||
if (typeof message.content === "string") {
|
||||
// Simple string content - merge with previous assistant message if possible
|
||||
mergeOrAddAssistantMessage(message.content, undefined, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Separate tool_use blocks from other content
|
||||
const toolUses: Anthropic.ToolUseBlockParam[] = []
|
||||
const otherContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_use") {
|
||||
toolUses.push(block)
|
||||
} else if (block.type === "text" || block.type === "image") {
|
||||
otherContent.push(block)
|
||||
}
|
||||
// Ignore other block types (assistant cannot send tool_result)
|
||||
}
|
||||
|
||||
// Convert text content - preserve empty strings
|
||||
let textContent: string | undefined
|
||||
if (otherContent.length > 0) {
|
||||
const texts = otherContent
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => (part as Anthropic.TextBlockParam).text)
|
||||
// If there were text blocks, join them (even if empty)
|
||||
// If there were no text blocks, textContent remains undefined
|
||||
if (texts.length > 0) {
|
||||
textContent = texts.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tool_use blocks to tool_calls
|
||||
let toolCalls: OpenAI.Chat.ChatCompletionMessageToolCall[] | undefined
|
||||
if (toolUses.length > 0) {
|
||||
toolCalls = toolUses.map((toolUse) => ({
|
||||
id: toolUse.id,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: toolUse.name,
|
||||
arguments: JSON.stringify(toolUse.input),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// If there are tool calls, we cannot merge (tool_calls must be in their own message)
|
||||
if (toolCalls && toolCalls.length > 0) {
|
||||
const assistantMessage: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: textContent || null,
|
||||
tool_calls: toolCalls,
|
||||
}
|
||||
result.push(assistantMessage)
|
||||
} else if (textContent !== undefined) {
|
||||
// No tool calls - can merge with previous assistant message
|
||||
// Note: textContent can be empty string "" which should be preserved
|
||||
mergeOrAddAssistantMessage(textContent, undefined, result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert user content blocks to OpenAI format
|
||||
*/
|
||||
function convertUserContent(content: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]): {
|
||||
content: string | (ContentPartText | ContentPartImage)[]
|
||||
hasImages: boolean
|
||||
} {
|
||||
const textParts: string[] = []
|
||||
const imageParts: ContentPartImage[] = []
|
||||
let hasImages = false
|
||||
|
||||
for (const part of content) {
|
||||
if (part.type === "text") {
|
||||
textParts.push(part.text)
|
||||
} else if (part.type === "image") {
|
||||
hasImages = true
|
||||
imageParts.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (hasImages) {
|
||||
const parts: (ContentPartText | ContentPartImage)[] = []
|
||||
if (textParts.length > 0) {
|
||||
parts.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
parts.push(...imageParts)
|
||||
return { content: parts, hasImages: true }
|
||||
}
|
||||
|
||||
return { content: textParts.join("\n"), hasImages: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge text content with the previous user message if possible, or add a new one
|
||||
*/
|
||||
function mergeOrAddUserMessage(content: string, result: Message[]): void {
|
||||
const lastMessage = result[result.length - 1]
|
||||
|
||||
if (lastMessage?.role === "user") {
|
||||
// Merge with previous user message
|
||||
if (typeof lastMessage.content === "string") {
|
||||
lastMessage.content = lastMessage.content + "\n" + content
|
||||
} else if (Array.isArray(lastMessage.content)) {
|
||||
// Previous message has array content - add text to it
|
||||
lastMessage.content.push({ type: "text", text: content })
|
||||
}
|
||||
} else {
|
||||
// Add new user message
|
||||
result.push({ role: "user", content })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge array content (with images) with the previous user message if possible, or add a new one
|
||||
*/
|
||||
function mergeOrAddUserMessageWithArrayContent(
|
||||
content: (ContentPartText | ContentPartImage)[],
|
||||
result: Message[],
|
||||
): void {
|
||||
const lastMessage = result[result.length - 1]
|
||||
|
||||
if (lastMessage?.role === "user") {
|
||||
// Merge with previous user message
|
||||
if (typeof lastMessage.content === "string") {
|
||||
// Convert string to array and append new content
|
||||
lastMessage.content = [{ type: "text", text: lastMessage.content }, ...content]
|
||||
} else if (Array.isArray(lastMessage.content)) {
|
||||
// Previous message has array content - append new content
|
||||
lastMessage.content.push(...content)
|
||||
}
|
||||
} else {
|
||||
// Add new user message
|
||||
result.push({ role: "user", content })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge text content with the previous assistant message if possible, or add a new one
|
||||
*/
|
||||
function mergeOrAddAssistantMessage(
|
||||
content: string | undefined,
|
||||
toolCalls: OpenAI.Chat.ChatCompletionMessageToolCall[] | undefined,
|
||||
result: Message[],
|
||||
): void {
|
||||
const lastMessage = result[result.length - 1]
|
||||
const hasContent = content !== undefined
|
||||
|
||||
// Can only merge if there are no tool_calls and the previous message is an assistant without tool_calls
|
||||
if (
|
||||
lastMessage?.role === "assistant" &&
|
||||
!toolCalls &&
|
||||
!(lastMessage as AssistantMessage).tool_calls &&
|
||||
hasContent &&
|
||||
content // Only merge non-empty content
|
||||
) {
|
||||
// Merge with previous assistant message
|
||||
if (typeof lastMessage.content === "string") {
|
||||
lastMessage.content = lastMessage.content + "\n" + content
|
||||
} else if (lastMessage.content === null || lastMessage.content === undefined) {
|
||||
lastMessage.content = content
|
||||
}
|
||||
} else if (hasContent || toolCalls) {
|
||||
// Add new assistant message (including empty string content)
|
||||
const assistantMessage: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: content ?? null,
|
||||
...(toolCalls && toolCalls.length > 0 && { tool_calls: toolCalls }),
|
||||
}
|
||||
result.push(assistantMessage)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue