mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-10 22:41:14 +00:00
fix(ai-sdk): preserve reasoning parts in message conversion (#11217)
* fix(ai-sdk): preserve reasoning parts in message conversion * fix(ai-sdk): convert message-level reasoning_content to reasoning part * fix(task): remove invalid openai-compatible from reasoning allowlist * feat: add isAiSdkProvider() method for dynamic AI SDK provider detection - Add isAiSdkProvider() method to ApiHandler interface - Default implementation in BaseProvider returns false - Override to return true in 11 AI SDK providers: deepseek, fireworks, mistral, groq, xai, cerebras, sambanova, huggingface, gemini, vertex, openai-compatible - Update Task.ts to use dynamic detection instead of hardcoded Set - Add method to FakeAIHandler and update test mocks * fix: handle reasoning parts in flattenAiSdkMessagesToStringContent - Strip reasoning parts when flattening messages for string-only models - Allow flattening when message contains only text and reasoning parts - Add tests for reasoning part handling in string-only model contexts This addresses the review feedback about ensuring flattenAiSdkMessagesToStringContent works correctly when reasoning parts are present (e.g., SambaNova DeepSeek). --------- Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
parent
934f34ea87
commit
1b75d59a68
19 changed files with 319 additions and 101 deletions
|
|
@ -117,6 +117,15 @@ export interface ApiHandler {
|
|||
* @returns A promise resolving to the token count
|
||||
*/
|
||||
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number>
|
||||
|
||||
/**
|
||||
* Indicates whether this provider uses the Vercel AI SDK for streaming.
|
||||
* AI SDK providers handle reasoning blocks differently and need to preserve
|
||||
* them in conversation history for proper round-tripping.
|
||||
*
|
||||
* @returns true if the provider uses AI SDK, false otherwise
|
||||
*/
|
||||
isAiSdkProvider(): boolean
|
||||
}
|
||||
|
||||
export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
||||
|
|
|
|||
|
|
@ -119,4 +119,12 @@ export abstract class BaseProvider implements ApiHandler {
|
|||
|
||||
return countTokens(content, { useWorker: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation returns false.
|
||||
* AI SDK providers should override this to return true.
|
||||
*/
|
||||
isAiSdkProvider(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,4 +156,8 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,4 +166,8 @@ export class DeepSeekHandler extends BaseProvider implements SingleCompletionHan
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,4 +78,8 @@ export class FakeAIHandler implements ApiHandler, SingleCompletionHandler {
|
|||
completePrompt(prompt: string): Promise<string> {
|
||||
return this.ai.completePrompt(prompt)
|
||||
}
|
||||
|
||||
isAiSdkProvider(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,4 +172,8 @@ export class FireworksHandler extends BaseProvider implements SingleCompletionHa
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,4 +397,8 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
return totalCost
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,4 +174,8 @@ export class GroqHandler extends BaseProvider implements SingleCompletionHandler
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,4 +208,8 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,4 +198,8 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,4 +186,8 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,4 +177,8 @@ export class SambaNovaHandler extends BaseProvider implements SingleCompletionHa
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -402,4 +402,8 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
return totalCost
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,4 +187,8 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
throw handleAiSdkError(error, "xAI")
|
||||
}
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,6 +308,97 @@ 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", () => {
|
||||
|
|
@ -817,5 +908,54 @@ describe("AI SDK conversion utilities", () => {
|
|||
|
||||
expect(result[0].content).toBe("\nHello")
|
||||
})
|
||||
|
||||
it("should strip reasoning parts and flatten text for string-only models", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [
|
||||
{ type: "reasoning" as const, text: "I am thinking about this..." },
|
||||
{ type: "text" as const, text: "Here is my answer" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = flattenAiSdkMessagesToStringContent(messages)
|
||||
|
||||
// Reasoning should be stripped, only text should remain
|
||||
expect(result[0].content).toBe("Here is my answer")
|
||||
})
|
||||
|
||||
it("should handle messages with only reasoning parts", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "reasoning" as const, text: "Only reasoning, no text" }],
|
||||
},
|
||||
]
|
||||
|
||||
const result = flattenAiSdkMessagesToStringContent(messages)
|
||||
|
||||
// Should flatten to empty string when only reasoning is present
|
||||
expect(result[0].content).toBe("")
|
||||
})
|
||||
|
||||
it("should not flatten if tool calls are present with reasoning", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [
|
||||
{ type: "reasoning" as const, text: "Thinking..." },
|
||||
{ type: "text" as const, text: "Using tool" },
|
||||
{ type: "tool-call" as const, toolCallId: "abc", toolName: "test", input: {} },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = flattenAiSdkMessagesToStringContent(messages)
|
||||
|
||||
// Should not flatten because there's a tool call
|
||||
expect(result[0]).toEqual(messages[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
}),
|
||||
createMessage: vitest.fn(),
|
||||
countTokens: vitest.fn(),
|
||||
isAiSdkProvider: vitest.fn().mockReturnValue(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ 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
|
||||
|
|
@ -136,21 +141,57 @@ export function convertToAiSdkMessages(
|
|||
for (const part of message.content) {
|
||||
if (part.type === "text") {
|
||||
textParts.push(part.text)
|
||||
} else if (part.type === "tool_use") {
|
||||
continue
|
||||
}
|
||||
|
||||
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") })
|
||||
}
|
||||
|
|
@ -226,10 +267,13 @@ export function flattenAiSdkMessagesToStringContent(
|
|||
// Handle assistant messages
|
||||
if (message.role === "assistant" && flattenAssistantMessages && Array.isArray(message.content)) {
|
||||
const parts = message.content as Array<{ type: string; text?: string }>
|
||||
// Only flatten if all parts are text (no tool calls)
|
||||
const allText = parts.every((part) => part.type === "text")
|
||||
if (allText && parts.length > 0) {
|
||||
const textContent = parts.map((part) => part.text || "").join("\n")
|
||||
// Only flatten if all parts are text or reasoning (no tool calls)
|
||||
// Reasoning parts are included in text to avoid sending multipart content to string-only models
|
||||
const allTextOrReasoning = parts.every((part) => part.type === "text" || part.type === "reasoning")
|
||||
if (allTextOrReasoning && parts.length > 0) {
|
||||
// Extract only text parts for the flattened content (reasoning is stripped for string-only models)
|
||||
const textParts = parts.filter((part) => part.type === "text")
|
||||
const textContent = textParts.map((part) => part.text || "").join("\n")
|
||||
return {
|
||||
...message,
|
||||
content: textContent,
|
||||
|
|
|
|||
|
|
@ -4564,14 +4564,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
|
||||
continue
|
||||
} else if (hasPlainTextReasoning) {
|
||||
// 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
|
||||
// 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 shouldPreserveForApi =
|
||||
this.api.getModel().info.preserveReasoning === true || this.api.isAiSdkProvider()
|
||||
|
||||
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,41 +219,33 @@ describe("Task reasoning preservation", () => {
|
|||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
|
||||
// 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}`
|
||||
}
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: finalAssistantMessage }],
|
||||
})
|
||||
// Verify that reasoning was stored as a separate reasoning block
|
||||
expect(addToApiHistorySpy).toHaveBeenCalledWith(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
|
||||
// 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
|
||||
// Verify the API conversation history contains the message with reasoning block
|
||||
expect(task.apiConversationHistory).toHaveLength(1)
|
||||
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...",
|
||||
)
|
||||
expect(task.apiConversationHistory[0].role).toBe("assistant")
|
||||
expect(task.apiConversationHistory[0].content).toEqual([
|
||||
{ type: "reasoning", text: reasoningMessage, summary: [] },
|
||||
{ type: "text", text: assistantMessage },
|
||||
])
|
||||
})
|
||||
|
||||
it("should NOT append reasoning to assistant message when preserveReasoning is false", async () => {
|
||||
it("should store reasoning blocks even when preserveReasoning is false", async () => {
|
||||
// Create a task instance
|
||||
const task = new Task({
|
||||
provider: mockProvider as ClineProvider,
|
||||
|
|
@ -279,36 +271,25 @@ describe("Task reasoning preservation", () => {
|
|||
// Mock the API conversation history
|
||||
task.apiConversationHistory = []
|
||||
|
||||
// Simulate adding an assistant message with reasoning
|
||||
// Add an assistant message while passing reasoning separately (Task does this in normal streaming).
|
||||
const assistantMessage = "Here is my response to your question."
|
||||
const reasoningMessage = "Let me think about this step by step. First, I need to..."
|
||||
|
||||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
|
||||
// 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].content[0] as { text: string }).text).toBe(
|
||||
"Here is my response to your question.",
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
expect((task.apiConversationHistory[0].content[0] as { text: string }).text).not.toContain("<think>")
|
||||
|
||||
// Verify the API conversation history contains a reasoning block (storage is unconditional)
|
||||
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 },
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle empty reasoning message gracefully when preserveReasoning is true", async () => {
|
||||
|
|
@ -340,29 +321,16 @@ describe("Task reasoning preservation", () => {
|
|||
const assistantMessage = "Here is my response."
|
||||
const reasoningMessage = "" // Empty reasoning
|
||||
|
||||
// Spy on addToApiConversationHistory
|
||||
const addToApiHistorySpy = vi.spyOn(task as any, "addToApiConversationHistory")
|
||||
await (task as any).addToApiConversationHistory(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: assistantMessage }],
|
||||
},
|
||||
reasoningMessage || undefined,
|
||||
)
|
||||
|
||||
// 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>")
|
||||
// Verify no reasoning blocks were added when reasoning is empty
|
||||
expect(task.apiConversationHistory[0].content).toEqual([{ type: "text", text: "Here is my response." }])
|
||||
})
|
||||
|
||||
it("should handle undefined preserveReasoning (defaults to false)", async () => {
|
||||
|
|
@ -394,20 +362,19 @@ describe("Task reasoning preservation", () => {
|
|||
const assistantMessage = "Here is my response."
|
||||
const reasoningMessage = "Some reasoning here."
|
||||
|
||||
// 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: assistantMessage }],
|
||||
},
|
||||
reasoningMessage,
|
||||
)
|
||||
|
||||
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>")
|
||||
// Verify reasoning is stored even when preserveReasoning is undefined
|
||||
expect(task.apiConversationHistory[0].content).toEqual([
|
||||
{ type: "reasoning", text: reasoningMessage, summary: [] },
|
||||
{ type: "text", text: assistantMessage },
|
||||
])
|
||||
})
|
||||
|
||||
it("should embed encrypted reasoning as first assistant content block", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue