fix: apply R1 format conversion to Mistral 2512 models

Extends the message format conversion logic to include Mistral 2512 models
(mistralai/*-2512) which require strict role alternation between user and
assistant messages, similar to DeepSeek R1 models.

This fixes the API error: "After the optional system message, conversation
roles must alternate user and assistant roles except for tool calls and
results."

- Updated openrouter.ts to detect Mistral 2512 models via regex pattern
- Added test coverage for both mistral-large-2512 and devstral-2512
- Reuses existing convertToR1Format function for consistent handling

Fixes #10045
This commit is contained in:
Roo Code 2025-12-12 12:45:35 +00:00
parent f97b5155ac
commit a81ad325bd
2 changed files with 91 additions and 1 deletions

View file

@ -489,6 +489,90 @@ describe("OpenRouterHandler", () => {
expect(endChunks).toHaveLength(1)
expect(endChunks[0].id).toBe("call_openrouter_test")
})
it("uses R1 format for Mistral 2512 models", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "mistralai/mistral-large-2512",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
id: "test-id",
choices: [{ delta: { content: "test response" } }],
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const systemPrompt = "test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "message 1" },
{ role: "user", content: "message 2" },
]
await handler.createMessage(systemPrompt, messages).next()
// Verify that convertToR1Format was used by checking messages structure
// Consecutive user messages should be merged into a single user message
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.arrayContaining([
expect.objectContaining({
role: "user",
// The system prompt and two user messages should be merged
content: expect.stringContaining("test system prompt"),
}),
]),
}),
undefined,
)
// Verify that messages were merged (should have fewer messages than original)
const callArgs = mockCreate.mock.calls[0][0]
// Original would have 3 messages (system converted to user + 2 user messages)
// After R1 format conversion, consecutive user messages should be merged into 1
expect(callArgs.messages).toHaveLength(1)
})
it("uses R1 format for Mistral devstral-2512 model", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "mistralai/devstral-2512",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
id: "test-id",
choices: [{ delta: { content: "test response" } }],
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const systemPrompt = "test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "message 1" },
{ role: "user", content: "message 2" },
]
await handler.createMessage(systemPrompt, messages).next()
// Verify R1 format conversion was applied
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.messages).toHaveLength(1)
expect(callArgs.messages[0].role).toBe("user")
})
})
describe("completePrompt", () => {

View file

@ -140,7 +140,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
]
// DeepSeek highly recommends using user instead of system role.
if (modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning") {
// Mistral 2512 models also require strict role alternation.
const requiresR1Format =
modelId.startsWith("deepseek/deepseek-r1") ||
modelId === "perplexity/sonar-reasoning" ||
/mistralai\/.*-2512/.test(modelId)
if (requiresR1Format) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}