mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
[FIX] Fix OpenAI Native handling of encrypted reasoning blocks to prevent error when condensing (#9263)
This commit is contained in:
parent
da461182ae
commit
4a9ff8f4ef
2 changed files with 159 additions and 38 deletions
|
|
@ -613,22 +613,36 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
const responseId = handler.getResponseId?.()
|
||||
const reasoningData = handler.getEncryptedContent?.()
|
||||
|
||||
// If we have encrypted_content, add it as a reasoning item before the assistant message
|
||||
if (reasoningData?.encrypted_content) {
|
||||
this.apiConversationHistory.push({
|
||||
type: "reasoning",
|
||||
summary: [],
|
||||
encrypted_content: reasoningData.encrypted_content,
|
||||
...(reasoningData.id ? { id: reasoningData.id } : {}),
|
||||
ts: Date.now(),
|
||||
} as any)
|
||||
}
|
||||
|
||||
const messageWithTs = {
|
||||
// Start from the original assistant message
|
||||
const messageWithTs: any = {
|
||||
...message,
|
||||
...(responseId ? { id: responseId } : {}),
|
||||
ts: Date.now(),
|
||||
}
|
||||
|
||||
// If we have encrypted_content, embed it as the first content block on the assistant message.
|
||||
// This keeps reasoning + assistant atomic for context management while still allowing providers
|
||||
// to receive a separate reasoning item when we build the request.
|
||||
if (reasoningData?.encrypted_content) {
|
||||
const reasoningBlock = {
|
||||
type: "reasoning",
|
||||
summary: [] as any[],
|
||||
encrypted_content: reasoningData.encrypted_content,
|
||||
...(reasoningData.id ? { id: reasoningData.id } : {}),
|
||||
}
|
||||
|
||||
if (typeof messageWithTs.content === "string") {
|
||||
messageWithTs.content = [
|
||||
reasoningBlock,
|
||||
{ type: "text", text: messageWithTs.content } satisfies Anthropic.Messages.TextBlockParam,
|
||||
]
|
||||
} else if (Array.isArray(messageWithTs.content)) {
|
||||
messageWithTs.content = [reasoningBlock, ...messageWithTs.content]
|
||||
} else if (!messageWithTs.content) {
|
||||
messageWithTs.content = [reasoningBlock]
|
||||
}
|
||||
}
|
||||
|
||||
this.apiConversationHistory.push(messageWithTs)
|
||||
} else {
|
||||
const messageWithTs = { ...message, ts: Date.now() }
|
||||
|
|
@ -2908,33 +2922,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
}
|
||||
}
|
||||
|
||||
// Properly type cleaned conversation history to include either standard Anthropic messages
|
||||
// or provider-specific reasoning items (for encrypted continuity).
|
||||
type ReasoningItemForRequest = {
|
||||
type: "reasoning"
|
||||
encrypted_content: string
|
||||
id?: string
|
||||
summary?: any[]
|
||||
}
|
||||
type CleanConversationMessage = Anthropic.Messages.MessageParam | ReasoningItemForRequest
|
||||
|
||||
const messagesSinceLastSummary = getMessagesSinceLastSummary(this.apiConversationHistory)
|
||||
const cleanConversationHistory: CleanConversationMessage[] = maybeRemoveImageBlocks(
|
||||
messagesSinceLastSummary,
|
||||
this.api,
|
||||
).map((msg: ApiMessage): CleanConversationMessage => {
|
||||
// Pass through reasoning items as-is (including id if present)
|
||||
if (msg.type === "reasoning") {
|
||||
return {
|
||||
type: "reasoning",
|
||||
summary: msg.summary,
|
||||
encrypted_content: msg.encrypted_content!,
|
||||
...(msg.id ? { id: msg.id } : {}),
|
||||
}
|
||||
}
|
||||
// For regular messages, just return role and content
|
||||
return { role: msg.role!, content: msg.content as Anthropic.Messages.ContentBlockParam[] | string }
|
||||
})
|
||||
const messagesWithoutImages = maybeRemoveImageBlocks(messagesSinceLastSummary, this.api)
|
||||
const cleanConversationHistory = this.buildCleanConversationHistory(messagesWithoutImages as ApiMessage[])
|
||||
|
||||
// Check auto-approval limits
|
||||
const approvalResult = await this.autoApprovalHandler.checkAutoApprovalLimits(
|
||||
|
|
@ -3166,6 +3156,91 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
return checkpointSave(this, force, suppressMessage)
|
||||
}
|
||||
|
||||
private buildCleanConversationHistory(
|
||||
messages: ApiMessage[],
|
||||
): Array<
|
||||
Anthropic.Messages.MessageParam | { type: "reasoning"; encrypted_content: string; id?: string; summary?: any[] }
|
||||
> {
|
||||
type ReasoningItemForRequest = {
|
||||
type: "reasoning"
|
||||
encrypted_content: string
|
||||
id?: string
|
||||
summary?: any[]
|
||||
}
|
||||
|
||||
const cleanConversationHistory: (Anthropic.Messages.MessageParam | ReasoningItemForRequest)[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
// Legacy path: standalone reasoning items stored as separate messages
|
||||
if (msg.type === "reasoning" && msg.encrypted_content) {
|
||||
cleanConversationHistory.push({
|
||||
type: "reasoning",
|
||||
summary: msg.summary,
|
||||
encrypted_content: msg.encrypted_content!,
|
||||
...(msg.id ? { id: msg.id } : {}),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Preferred path: assistant message with embedded reasoning as first content block
|
||||
if (msg.role === "assistant") {
|
||||
const rawContent = msg.content
|
||||
|
||||
const contentArray: Anthropic.Messages.ContentBlockParam[] = Array.isArray(rawContent)
|
||||
? (rawContent as Anthropic.Messages.ContentBlockParam[])
|
||||
: rawContent !== undefined
|
||||
? ([
|
||||
{ type: "text", text: rawContent } satisfies Anthropic.Messages.TextBlockParam,
|
||||
] as Anthropic.Messages.ContentBlockParam[])
|
||||
: []
|
||||
|
||||
const [first, ...rest] = contentArray
|
||||
|
||||
const hasEmbeddedReasoning =
|
||||
first && (first as any).type === "reasoning" && typeof (first as any).encrypted_content === "string"
|
||||
|
||||
if (hasEmbeddedReasoning) {
|
||||
const reasoningBlock = first as any
|
||||
|
||||
// Emit a separate reasoning item for the provider
|
||||
cleanConversationHistory.push({
|
||||
type: "reasoning",
|
||||
summary: reasoningBlock.summary ?? [],
|
||||
encrypted_content: reasoningBlock.encrypted_content,
|
||||
...(reasoningBlock.id ? { id: reasoningBlock.id } : {}),
|
||||
})
|
||||
|
||||
// Build assistant message without the embedded reasoning block
|
||||
let assistantContent: Anthropic.Messages.MessageParam["content"]
|
||||
|
||||
if (rest.length === 0) {
|
||||
assistantContent = ""
|
||||
} else if (rest.length === 1 && rest[0].type === "text") {
|
||||
assistantContent = (rest[0] as Anthropic.Messages.TextBlockParam).text
|
||||
} else {
|
||||
assistantContent = rest
|
||||
}
|
||||
|
||||
cleanConversationHistory.push({
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
} satisfies Anthropic.Messages.MessageParam)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Default path for regular messages (no embedded reasoning)
|
||||
if (msg.role) {
|
||||
cleanConversationHistory.push({
|
||||
role: msg.role,
|
||||
content: msg.content as Anthropic.Messages.ContentBlockParam[] | string,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return cleanConversationHistory
|
||||
}
|
||||
public async checkpointRestore(options: CheckpointRestoreOptions) {
|
||||
return checkpointRestore(this, options)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -325,4 +325,50 @@ describe("Task reasoning preservation", () => {
|
|||
expect(task.apiConversationHistory[0].content[0].text).toBe("Here is my response.")
|
||||
expect(task.apiConversationHistory[0].content[0].text).not.toContain("<think>")
|
||||
})
|
||||
|
||||
it("should embed encrypted reasoning as first assistant content block", async () => {
|
||||
const task = new Task({
|
||||
provider: mockProvider as ClineProvider,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
task: "Test task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
// Avoid disk writes in this test
|
||||
;(task as any).saveApiConversationHistory = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
// Mock API handler to provide encrypted reasoning data and response id
|
||||
task.api = {
|
||||
getEncryptedContent: vi.fn().mockReturnValue({
|
||||
encrypted_content: "encrypted_payload",
|
||||
id: "rs_test",
|
||||
}),
|
||||
getResponseId: vi.fn().mockReturnValue("resp_test"),
|
||||
} as any
|
||||
|
||||
await (task as any).addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Here is my response." }],
|
||||
})
|
||||
|
||||
expect(task.apiConversationHistory).toHaveLength(1)
|
||||
const stored = task.apiConversationHistory[0] as any
|
||||
|
||||
expect(stored.role).toBe("assistant")
|
||||
expect(Array.isArray(stored.content)).toBe(true)
|
||||
expect(stored.id).toBe("resp_test")
|
||||
|
||||
const [reasoningBlock, textBlock] = stored.content
|
||||
|
||||
expect(reasoningBlock).toMatchObject({
|
||||
type: "reasoning",
|
||||
encrypted_content: "encrypted_payload",
|
||||
id: "rs_test",
|
||||
})
|
||||
|
||||
expect(textBlock).toMatchObject({
|
||||
type: "text",
|
||||
text: "Here is my response.",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue