mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Revert "fix(ai-sdk): preserve reasoning parts in message conversion" (#11216)
Revert "fix(ai-sdk): preserve reasoning parts in message conversion (#11196)"
This reverts commit 227b9796d3.
This commit is contained in:
parent
227b9796d3
commit
934f34ea87
4 changed files with 96 additions and 209 deletions
|
|
@ -308,97 +308,6 @@ describe("AI SDK conversion utilities", () => {
|
|||
content: [{ type: "text", text: "" }],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts assistant reasoning blocks", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning" as any, text: "Thinking..." },
|
||||
{ type: "text", text: "Answer" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToAiSdkMessages(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Thinking..." },
|
||||
{ type: "text", text: "Answer" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts assistant thinking blocks to reasoning", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking" as any, thinking: "Deep thought", signature: "sig" },
|
||||
{ type: "text", text: "OK" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToAiSdkMessages(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Deep thought" },
|
||||
{ type: "text", text: "OK" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts assistant message-level reasoning_content to reasoning part", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Answer" }],
|
||||
reasoning_content: "Thinking...",
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = convertToAiSdkMessages(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Thinking..." },
|
||||
{ type: "text", text: "Answer" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("prefers message-level reasoning_content over reasoning blocks", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning" as any, text: "BLOCK" },
|
||||
{ type: "text", text: "Answer" },
|
||||
],
|
||||
reasoning_content: "MSG",
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = convertToAiSdkMessages(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "MSG" },
|
||||
{ type: "text", text: "Answer" },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertToolsForAiSdk", () => {
|
||||
|
|
|
|||
|
|
@ -126,11 +126,6 @@ export function convertToAiSdkMessages(
|
|||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const textParts: string[] = []
|
||||
const reasoningParts: string[] = []
|
||||
const reasoningContent = (() => {
|
||||
const maybe = (message as unknown as { reasoning_content?: unknown }).reasoning_content
|
||||
return typeof maybe === "string" && maybe.length > 0 ? maybe : undefined
|
||||
})()
|
||||
const toolCalls: Array<{
|
||||
type: "tool-call"
|
||||
toolCallId: string
|
||||
|
|
@ -141,57 +136,21 @@ export function convertToAiSdkMessages(
|
|||
for (const part of message.content) {
|
||||
if (part.type === "text") {
|
||||
textParts.push(part.text)
|
||||
continue
|
||||
}
|
||||
|
||||
if (part.type === "tool_use") {
|
||||
} else if (part.type === "tool_use") {
|
||||
toolCalls.push({
|
||||
type: "tool-call",
|
||||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
input: part.input,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Some providers (DeepSeek, Gemini, etc.) require reasoning to be round-tripped.
|
||||
// Task stores reasoning as a content block (type: "reasoning") and Anthropic extended
|
||||
// thinking as (type: "thinking"). Convert both to AI SDK's reasoning part.
|
||||
if ((part as unknown as { type?: string }).type === "reasoning") {
|
||||
// If message-level reasoning_content is present, treat it as canonical and
|
||||
// avoid mixing it with content-block reasoning (which can cause duplication).
|
||||
if (reasoningContent) continue
|
||||
|
||||
const text = (part as unknown as { text?: string }).text
|
||||
if (typeof text === "string" && text.length > 0) {
|
||||
reasoningParts.push(text)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ((part as unknown as { type?: string }).type === "thinking") {
|
||||
if (reasoningContent) continue
|
||||
|
||||
const thinking = (part as unknown as { thinking?: string }).thinking
|
||||
if (typeof thinking === "string" && thinking.length > 0) {
|
||||
reasoningParts.push(thinking)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const content: Array<
|
||||
| { type: "reasoning"; text: string }
|
||||
| { type: "text"; text: string }
|
||||
| { type: "tool-call"; toolCallId: string; toolName: string; input: unknown }
|
||||
> = []
|
||||
|
||||
if (reasoningContent) {
|
||||
content.push({ type: "reasoning", text: reasoningContent })
|
||||
} else if (reasoningParts.length > 0) {
|
||||
content.push({ type: "reasoning", text: reasoningParts.join("") })
|
||||
}
|
||||
|
||||
if (textParts.length > 0) {
|
||||
content.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4564,28 +4564,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
|
||||
continue
|
||||
} else if (hasPlainTextReasoning) {
|
||||
// Preserve plain-text reasoning blocks for:
|
||||
// - models explicitly opting in via preserveReasoning
|
||||
// - AI SDK providers (provider packages decide what to include in the native request)
|
||||
const aiSdkProviders = new Set([
|
||||
"deepseek",
|
||||
"fireworks",
|
||||
"moonshot",
|
||||
"mistral",
|
||||
"groq",
|
||||
"xai",
|
||||
"cerebras",
|
||||
"sambanova",
|
||||
"huggingface",
|
||||
])
|
||||
|
||||
const shouldPreserveForApi =
|
||||
this.api.getModel().info.preserveReasoning === true ||
|
||||
aiSdkProviders.has(this.apiConfiguration.apiProvider ?? "")
|
||||
|
||||
// Check if the model's preserveReasoning flag is set
|
||||
// If true, include the reasoning block in API requests
|
||||
// If false/undefined, strip it out (stored for history only, not sent back to API)
|
||||
const shouldPreserveForApi = this.api.getModel().info.preserveReasoning === true
|
||||
let assistantContent: Anthropic.Messages.MessageParam["content"]
|
||||
|
||||
if (shouldPreserveForApi) {
|
||||
// Include reasoning block in the content sent to API
|
||||
assistantContent = contentArray
|
||||
} else {
|
||||
// Strip reasoning out - stored for history only, not sent back to API
|
||||
|
|
|
|||
|
|
@ -219,33 +219,41 @@ describe("Task reasoning preservation", () => {
|
|||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
// Simulate what happens in the streaming loop when preserveReasoning is true
|
||||
let finalAssistantMessage = assistantMessage
|
||||
if (reasoningMessage && task.api.getModel().info.preserveReasoning) {
|
||||
finalAssistantMessage = `<think>${reasoningMessage}</think>\n${assistantMessage}`
|
||||
}
|
||||
|
||||
// Verify that reasoning was stored as a separate reasoning block
|
||||
expect(addToApiHistorySpy).toHaveBeenCalledWith(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: finalAssistantMessage }],
|
||||
})
|
||||
|
||||
// Verify the API conversation history contains the message with reasoning block
|
||||
// Verify that reasoning was prepended in <think> tags to the assistant message
|
||||
expect(addToApiHistorySpy).toHaveBeenCalledWith({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<think>Let me think about this step by step. First, I need to...</think>\nHere is my response to your question.",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Verify the API conversation history contains the message with reasoning
|
||||
expect(task.apiConversationHistory).toHaveLength(1)
|
||||
expect(task.apiConversationHistory[0].role).toBe("assistant")
|
||||
expect(task.apiConversationHistory[0].content).toEqual([
|
||||
{ type: "reasoning", text: reasoningMessage, summary: [] },
|
||||
{ type: "text", text: assistantMessage },
|
||||
])
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toContain("<think>")
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toContain("</think>")
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toContain(
|
||||
"Here is my response to your question.",
|
||||
)
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toContain(
|
||||
"Let me think about this step by step. First, I need to...",
|
||||
)
|
||||
})
|
||||
|
||||
it("should store reasoning blocks even when preserveReasoning is false", async () => {
|
||||
it("should NOT append reasoning to assistant message when preserveReasoning is false", async () => {
|
||||
// Create a task instance
|
||||
const task = new Task({
|
||||
provider: mockProvider as ClineProvider,
|
||||
|
|
@ -271,25 +279,36 @@ describe("Task reasoning preservation", () => {
|
|||
// Mock the API conversation history
|
||||
task.apiConversationHistory = []
|
||||
|
||||
// Add an assistant message while passing reasoning separately (Task does this in normal streaming).
|
||||
// Simulate adding an assistant message with reasoning
|
||||
const assistantMessage = "Here is my response to your question."
|
||||
const reasoningMessage = "Let me think about this step by step. First, I need to..."
|
||||
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
|
||||
// Verify the API conversation history contains a reasoning block (storage is unconditional)
|
||||
// Simulate what happens in the streaming loop when preserveReasoning is false
|
||||
let finalAssistantMessage = assistantMessage
|
||||
if (reasoningMessage && task.api.getModel().info.preserveReasoning) {
|
||||
finalAssistantMessage = `<think>${reasoningMessage}</think>\n${assistantMessage}`
|
||||
}
|
||||
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: finalAssistantMessage }],
|
||||
})
|
||||
|
||||
// Verify that reasoning was NOT appended to the assistant message
|
||||
expect(addToApiHistorySpy).toHaveBeenCalledWith({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Here is my response to your question." }],
|
||||
})
|
||||
|
||||
// Verify the API conversation history does NOT contain reasoning
|
||||
expect(task.apiConversationHistory).toHaveLength(1)
|
||||
expect(task.apiConversationHistory[0].role).toBe("assistant")
|
||||
expect(task.apiConversationHistory[0].content).toEqual([
|
||||
{ type: "reasoning", text: reasoningMessage, summary: [] },
|
||||
{ type: "text", text: assistantMessage },
|
||||
])
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toBe(
|
||||
"Here is my response to your question.",
|
||||
)
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).not.toContain("<think>")
|
||||
})
|
||||
|
||||
it("should handle empty reasoning message gracefully when preserveReasoning is true", async () => {
|
||||
|
|
@ -321,16 +340,29 @@ describe("Task reasoning preservation", () => {
|
|||
const assistantMessage = "Here is my response."
|
||||
const reasoningMessage = "" // Empty reasoning
|
||||
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage || undefined,
|
||||
)
|
||||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
|
||||
// Verify no reasoning blocks were added when reasoning is empty
|
||||
expect(task.apiConversationHistory[0].content).toEqual([{ type: "text", text: "Here is my response." }])
|
||||
// Simulate what happens in the streaming loop
|
||||
let finalAssistantMessage = assistantMessage
|
||||
if (reasoningMessage && task.api.getModel().info.preserveReasoning) {
|
||||
finalAssistantMessage = `<think>${reasoningMessage}</think>\n${assistantMessage}`
|
||||
}
|
||||
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: finalAssistantMessage }],
|
||||
})
|
||||
|
||||
// Verify that no reasoning tags were added when reasoning is empty
|
||||
expect(addToApiHistorySpy).toHaveBeenCalledWith({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Here is my response." }],
|
||||
})
|
||||
|
||||
// Verify the message doesn't contain reasoning tags
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toBe("Here is my response.")
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).not.toContain("<think>")
|
||||
})
|
||||
|
||||
it("should handle undefined preserveReasoning (defaults to false)", async () => {
|
||||
|
|
@ -362,19 +394,20 @@ describe("Task reasoning preservation", () => {
|
|||
const assistantMessage = "Here is my response."
|
||||
const reasoningMessage = "Some reasoning here."
|
||||
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
// Simulate what happens in the streaming loop
|
||||
let finalAssistantMessage = assistantMessage
|
||||
if (reasoningMessage && task.api.getModel().info.preserveReasoning) {
|
||||
finalAssistantMessage = `<think>${reasoningMessage}</think>\n${assistantMessage}`
|
||||
}
|
||||
|
||||
// Verify reasoning is stored even when preserveReasoning is undefined
|
||||
expect(task.apiConversationHistory[0].content).toEqual([
|
||||
{ type: "reasoning", text: reasoningMessage, summary: [] },
|
||||
{ type: "text", text: assistantMessage },
|
||||
])
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: finalAssistantMessage }],
|
||||
})
|
||||
|
||||
// Verify reasoning was NOT prepended (undefined defaults to false)
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).toBe("Here is my response.")
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).not.toContain("<think>")
|
||||
})
|
||||
|
||||
it("should embed encrypted reasoning as first assistant content block", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue