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:
Matt Rubens 2026-02-05 07:40:36 -08:00 committed by GitHub
parent 227b9796d3
commit 934f34ea87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 96 additions and 209 deletions

View file

@ -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", () => {

View file

@ -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") })
}

View file

@ -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

View file

@ -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 () => {