fix: preserve id for reasoning.encrypted blocks in OpenAI message transform

Gemini 3 models require the id field to be preserved for reasoning.encrypted
blocks to link thought signatures to function calls. Without this id, the API
returns an error about missing thought_signature in functionCall parts.

The id field is now only stripped for non-encrypted reasoning types (like
reasoning.text and reasoning.summary), which are used internally for accumulation.
This commit is contained in:
Roo Code 2025-12-23 19:09:41 +00:00
parent 40812dcb64
commit 6916852b18
2 changed files with 152 additions and 1 deletions

View file

@ -401,4 +401,148 @@ describe("convertToOpenAiMessages", () => {
expect(openAiMessages[0].role).toBe("user")
})
})
describe("reasoning_details handling", () => {
it("should strip id from reasoning.text blocks", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "text",
text: "I will help you.",
},
],
reasoning_details: [
{
type: "reasoning.text",
text: "Let me think about this...",
format: "google-gemini-v1",
index: 0,
id: "internal_accumulation_id",
},
],
} as any,
]
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
const assistantMessage = openAiMessages[0] as any
expect(assistantMessage.reasoning_details).toHaveLength(1)
expect(assistantMessage.reasoning_details[0]).toEqual({
type: "reasoning.text",
text: "Let me think about this...",
format: "google-gemini-v1",
index: 0,
})
expect(assistantMessage.reasoning_details[0].id).toBeUndefined()
})
it("should preserve id for reasoning.encrypted blocks (required by Gemini 3)", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "tool_new_task_k7LktBScQZtG5uIJYm6g",
name: "new_task",
input: { mode: "code", message: "test" },
},
],
reasoning_details: [
{
type: "reasoning.text",
text: "Let me think...",
format: "google-gemini-v1",
index: 0,
id: "internal_id_to_strip",
},
{
type: "reasoning.encrypted",
data: "encrypted_thought_signature_data",
id: "tool_new_task_k7LktBScQZtG5uIJYm6g",
format: "google-gemini-v1",
index: 0,
},
],
} as any,
]
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
const assistantMessage = openAiMessages[0] as any
expect(assistantMessage.reasoning_details).toHaveLength(2)
// reasoning.text should have id stripped
expect(assistantMessage.reasoning_details[0]).toEqual({
type: "reasoning.text",
text: "Let me think...",
format: "google-gemini-v1",
index: 0,
})
expect(assistantMessage.reasoning_details[0].id).toBeUndefined()
// reasoning.encrypted should preserve id (required for tool call thought signatures)
expect(assistantMessage.reasoning_details[1]).toEqual({
type: "reasoning.encrypted",
data: "encrypted_thought_signature_data",
id: "tool_new_task_k7LktBScQZtG5uIJYm6g",
format: "google-gemini-v1",
index: 0,
})
})
it("should handle reasoning_details without id field", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "text",
text: "Response text",
},
],
reasoning_details: [
{
type: "reasoning.summary",
summary: "Summary of reasoning",
format: "google-gemini-v1",
index: 0,
},
],
} as any,
]
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
const assistantMessage = openAiMessages[0] as any
expect(assistantMessage.reasoning_details).toHaveLength(1)
expect(assistantMessage.reasoning_details[0]).toEqual({
type: "reasoning.summary",
summary: "Summary of reasoning",
format: "google-gemini-v1",
index: 0,
})
})
it("should not add reasoning_details if not present", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "text",
text: "Simple response",
},
],
},
]
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
const assistantMessage = openAiMessages[0] as any
expect(assistantMessage.reasoning_details).toBeUndefined()
})
})
})

View file

@ -190,9 +190,16 @@ export function convertToOpenAiMessages(
}
// Add reasoning_details first (before tool_calls) to preserve provider-expected order
// Strip the id field from each reasoning detail as it's only used internally for accumulation
// Strip the id field from reasoning details as it's only used internally for accumulation,
// EXCEPT for reasoning.encrypted which requires the id (tool call ID) to link thought
// signatures to function calls (required by Gemini 3 models via OpenRouter)
if (messageWithDetails.reasoning_details && Array.isArray(messageWithDetails.reasoning_details)) {
baseMessage.reasoning_details = messageWithDetails.reasoning_details.map((detail: any) => {
// Keep id for reasoning.encrypted (tool call thought signatures)
// Strip id for other types (used internally for accumulation)
if (detail.type === "reasoning.encrypted") {
return detail
}
const { id, ...rest } = detail
return rest
})