mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix(openrouter): Fix reasoning details preservation and schema validation
- Re-index reasoning_details sequentially to prevent duplicate indices (fixes 400 error) - Ensure reasoning_details is omitted if empty to avoid invalid empty array error - Prevent creation of invalid empty text blocks from metadata-only updates - Force creation of text block in Task.ts if reasoning_details exist but text is empty, ensuring preservation
This commit is contained in:
parent
a8062124e8
commit
d19f7bdee0
3 changed files with 141 additions and 11 deletions
|
|
@ -128,4 +128,125 @@ describe("convertToOpenAiMessages", () => {
|
|||
expect(toolMessage.tool_call_id).toBe("weather-123")
|
||||
expect(toolMessage.content).toBe("Current temperature in London: 20°C")
|
||||
})
|
||||
it("should handle assistant messages with reasoning details and preserve index field", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "I will now reason about this.",
|
||||
// @ts-ignore
|
||||
reasoning_details: [
|
||||
{
|
||||
type: "reasoning.text",
|
||||
text: "This is the first part",
|
||||
index: 0,
|
||||
signature: "sig1",
|
||||
format: "format1",
|
||||
},
|
||||
{
|
||||
type: "reasoning.text",
|
||||
text: " of the reasoning.",
|
||||
index: 0,
|
||||
signature: "sig1",
|
||||
format: "format1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
|
||||
expect(openAiMessages).toHaveLength(1)
|
||||
|
||||
const assistantMessage = openAiMessages[0] as any
|
||||
expect(assistantMessage.role).toBe("assistant")
|
||||
expect(assistantMessage.content).toBe("I will now reason about this.")
|
||||
expect(assistantMessage.reasoning_details).toHaveLength(1)
|
||||
expect(assistantMessage.reasoning_details[0]).toEqual({
|
||||
type: "reasoning.text",
|
||||
text: "This is the first part of the reasoning.",
|
||||
signature: "sig1",
|
||||
format: "format1",
|
||||
index: 0,
|
||||
})
|
||||
expect(assistantMessage.reasoning_details[0]).toHaveProperty("index")
|
||||
})
|
||||
it("should not include reasoning_details if they consolidate to empty array", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Response.",
|
||||
// @ts-ignore
|
||||
reasoning_details: [
|
||||
{
|
||||
index: 0,
|
||||
// No text, no data
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
|
||||
expect(openAiMessages).toHaveLength(1)
|
||||
|
||||
const assistantMessage = openAiMessages[0] as any
|
||||
expect(assistantMessage.role).toBe("assistant")
|
||||
expect(assistantMessage.reasoning_details).toBeUndefined()
|
||||
})
|
||||
it("should re-index reasoning details sequentially", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Response.",
|
||||
// @ts-ignore
|
||||
reasoning_details: [
|
||||
{
|
||||
type: "reasoning.text",
|
||||
text: "Part 1",
|
||||
index: 0,
|
||||
format: "fmt",
|
||||
},
|
||||
{
|
||||
type: "reasoning.encrypted",
|
||||
data: "data1",
|
||||
index: 0,
|
||||
format: "fmt",
|
||||
},
|
||||
{
|
||||
type: "reasoning.text",
|
||||
text: "Part 2",
|
||||
index: 5, // Gap in index
|
||||
format: "fmt",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
|
||||
const assistantMessage = openAiMessages[0] as any
|
||||
const reasoning = assistantMessage.reasoning_details
|
||||
|
||||
expect(reasoning).toHaveLength(3)
|
||||
expect(reasoning[0].index).toBe(0)
|
||||
expect(reasoning[0].text).toBe("Part 1")
|
||||
|
||||
expect(reasoning[1].index).toBe(1)
|
||||
expect(reasoning[1].data).toBe("data1")
|
||||
|
||||
expect(reasoning[2].index).toBe(2)
|
||||
expect(reasoning[2].text).toBe("Part 2")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ export function convertToOpenAiMessages(
|
|||
},
|
||||
}))
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const consolidatedReasoning =
|
||||
reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails) : undefined
|
||||
|
||||
openAiMessages.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
|
|
@ -155,7 +159,7 @@ export function convertToOpenAiMessages(
|
|||
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
|
||||
// @ts-ignore-next-line
|
||||
reasoning_details:
|
||||
reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails) : undefined,
|
||||
consolidatedReasoning && consolidatedReasoning.length > 0 ? consolidatedReasoning : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -203,18 +207,21 @@ function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): Reaso
|
|||
|
||||
// Consolidate each group
|
||||
const consolidated: ReasoningDetail[] = []
|
||||
let outputIndex = 0
|
||||
|
||||
for (const [index, details] of groupedByIndex.entries()) {
|
||||
for (const [_, details] of groupedByIndex.entries()) {
|
||||
// Concatenate all text parts
|
||||
let concatenatedText = ""
|
||||
let hasText = false
|
||||
let signature: string | undefined
|
||||
let id: string | undefined
|
||||
let format = "unknown"
|
||||
let type = "reasoning.text"
|
||||
|
||||
for (const detail of details) {
|
||||
if (detail.text) {
|
||||
if (detail.text !== undefined) {
|
||||
concatenatedText += detail.text
|
||||
hasText = true
|
||||
}
|
||||
// Keep the signature from the last item that has one
|
||||
if (detail.signature) {
|
||||
|
|
@ -233,15 +240,16 @@ function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): Reaso
|
|||
}
|
||||
}
|
||||
|
||||
// Create consolidated entry for text
|
||||
if (concatenatedText) {
|
||||
// Create consolidated entry for text if any text parts were found
|
||||
// This avoids creating text entries for purely encrypted blocks or metadata-only updates that belong to encrypted blocks
|
||||
if (hasText) {
|
||||
const consolidatedEntry: ReasoningDetail = {
|
||||
type: type,
|
||||
text: concatenatedText,
|
||||
signature: signature,
|
||||
id: id,
|
||||
format: format,
|
||||
index: index,
|
||||
index: outputIndex++,
|
||||
}
|
||||
consolidated.push(consolidatedEntry)
|
||||
}
|
||||
|
|
@ -256,7 +264,7 @@ function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): Reaso
|
|||
signature: detail.signature,
|
||||
id: detail.id,
|
||||
format: detail.format,
|
||||
index: index,
|
||||
index: outputIndex++,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2657,8 +2657,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Check if we have any content to process (text or tool uses)
|
||||
const hasTextContent = assistantMessage.length > 0
|
||||
const hasToolUses = this.assistantMessageContent.some((block) => block.type === "tool_use")
|
||||
const hasReasoningDetails = reasoningDetails.length > 0
|
||||
|
||||
if (hasTextContent || hasToolUses) {
|
||||
if (hasTextContent || hasToolUses || hasReasoningDetails) {
|
||||
// Display grounding sources to the user if they exist
|
||||
if (pendingGroundingSources.length > 0) {
|
||||
const citationLinks = pendingGroundingSources.map((source, i) => `[${i + 1}](${source.url})`)
|
||||
|
|
@ -2672,11 +2673,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Build the assistant message content array
|
||||
const assistantContent: Array<Anthropic.TextBlockParam | Anthropic.ToolUseBlockParam> = []
|
||||
|
||||
// Add text content if present
|
||||
if (assistantMessage) {
|
||||
// Add text content if present, or if we have reasoning details that need a text block to be attached to
|
||||
if (assistantMessage || hasReasoningDetails) {
|
||||
assistantContent.push({
|
||||
type: "text" as const,
|
||||
text: assistantMessage,
|
||||
text: assistantMessage, // This will be empty string if !assistantMessage, which is fine
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue