From 0a28df132dec980f3e76a3bc076983e7e7df0566 Mon Sep 17 00:00:00 2001 From: chenxi <16267732+chenxi-null@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:21:27 +0800 Subject: [PATCH] fix(deepseek-reasoner) doesn't support successive user or assistant messages (#5051) fix(deepseek-reasoner) does not support successive user or assistant messages --- src/main/services/WindowService.ts | 1 + .../providers/AiProvider/OpenAIProvider.ts | 6 +- .../src/services/ModelMessageService.ts | 49 +++++++ .../__tests__/ModelMessageService.test.ts | 124 ++++++++++++++++++ 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/services/ModelMessageService.ts create mode 100644 src/renderer/src/services/__tests__/ModelMessageService.test.ts diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 7c28709c..cc839ad6 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -243,6 +243,7 @@ export class WindowService { private loadMainWindowContent(mainWindow: BrowserWindow) { if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + // mainWindow.webContents.openDevTools() } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 4373c85c..ea67862c 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -19,6 +19,7 @@ import { filterEmptyMessages, filterUserRoleStartMessages } from '@renderer/services/MessagesService' +import { processReqMessages } from '@renderer/services/ModelMessageService' import store from '@renderer/store' import { Assistant, @@ -504,7 +505,10 @@ export default class OpenAIProvider extends BaseProvider { await processToolUses(content, idx) } - // console.log('reqMessages', reqMessages) + + // console.log('[before] reqMessages', reqMessages) + reqMessages = processReqMessages(model, reqMessages) + // console.log('[after] reqMessages', reqMessages) const stream = await this.sdk.chat.completions // @ts-ignore key is not typed .create( diff --git a/src/renderer/src/services/ModelMessageService.ts b/src/renderer/src/services/ModelMessageService.ts new file mode 100644 index 00000000..9ce18859 --- /dev/null +++ b/src/renderer/src/services/ModelMessageService.ts @@ -0,0 +1,49 @@ +import { Model } from '@renderer/types' +import { ChatCompletionMessageParam } from 'openai/resources' + +export function processReqMessages( + model: Model, + reqMessages: ChatCompletionMessageParam[] +): ChatCompletionMessageParam[] { + if (!needStrictlyInterleaveUserAndAssistantMessages(model)) { + return reqMessages + } + + return mergeSameRoleMessages(reqMessages) +} + +function needStrictlyInterleaveUserAndAssistantMessages(model: Model) { + return model.id === 'deepseek-reasoner' +} + +/** + * Merge successive messages with the same role + */ +function mergeSameRoleMessages(messages: ChatCompletionMessageParam[]): ChatCompletionMessageParam[] { + const split = '\n' + const processedMessages: ChatCompletionMessageParam[] = [] + let currentGroup: ChatCompletionMessageParam[] = [] + + for (const message of messages) { + if (currentGroup.length === 0 || currentGroup[0].role === message.role) { + currentGroup.push(message) + } else { + // merge the current group and add to processed messages + processedMessages.push({ + ...currentGroup[0], + content: currentGroup.map((m) => m.content).join(split) + }) + currentGroup = [message] + } + } + + // process the last group + if (currentGroup.length > 0) { + processedMessages.push({ + ...currentGroup[0], + content: currentGroup.map((m) => m.content).join(split) + }) + } + + return processedMessages +} diff --git a/src/renderer/src/services/__tests__/ModelMessageService.test.ts b/src/renderer/src/services/__tests__/ModelMessageService.test.ts new file mode 100644 index 00000000..621e49d6 --- /dev/null +++ b/src/renderer/src/services/__tests__/ModelMessageService.test.ts @@ -0,0 +1,124 @@ +import assert from 'node:assert' +import { test } from 'node:test' + +import { ChatCompletionMessageParam } from 'openai/resources' + +const { processReqMessages } = require('../ModelMessageService') + +test('ModelMessageService', async (t) => { + const mockMessages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'First question' }, + { role: 'user', content: 'Additional context' }, + { role: 'assistant', content: 'First answer' }, + { role: 'assistant', content: 'Additional information' }, + { role: 'user', content: 'Second question' }, + { role: 'assistant', content: 'Second answer' } + ] + + await t.test('should merge successive messages with same role for deepseek-reasoner model', () => { + const model = { id: 'deepseek-reasoner' } + const result = processReqMessages(model, mockMessages) + + assert.strictEqual(result.length, 4) + assert.deepStrictEqual(result[0], { + role: 'user', + content: 'First question\nAdditional context' + }) + assert.deepStrictEqual(result[1], { + role: 'assistant', + content: 'First answer\nAdditional information' + }) + assert.deepStrictEqual(result[2], { + role: 'user', + content: 'Second question' + }) + assert.deepStrictEqual(result[3], { + role: 'assistant', + content: 'Second answer' + }) + }) + + await t.test('should not merge messages for other models', () => { + const model = { id: 'gpt-4' } + const result = processReqMessages(model, mockMessages) + + assert.strictEqual(result.length, mockMessages.length) + assert.deepStrictEqual(result, mockMessages) + }) + + await t.test('should handle empty messages array', () => { + const model = { id: 'deepseek-reasoner' } + const result = processReqMessages(model, []) + + assert.strictEqual(result.length, 0) + assert.deepStrictEqual(result, []) + }) + + await t.test('should handle single message', () => { + const model = { id: 'deepseek-reasoner' } + const singleMessage = [{ role: 'user', content: 'Single message' }] + const result = processReqMessages(model, singleMessage) + + assert.strictEqual(result.length, 1) + assert.deepStrictEqual(result, singleMessage) + }) + + await t.test('should preserve other message properties when merging', () => { + const model = { id: 'deepseek-reasoner' } + const messagesWithProps = [ + { + role: 'user', + content: 'First message', + name: 'user1', + function_call: { name: 'test', arguments: '{}' } + }, + { + role: 'user', + content: 'Second message', + name: 'user1' + } + ] as ChatCompletionMessageParam[] + + const result = processReqMessages(model, messagesWithProps) + + assert.strictEqual(result.length, 1) + assert.deepStrictEqual(result[0], { + role: 'user', + content: 'First message\nSecond message', + name: 'user1', + function_call: { name: 'test', arguments: '{}' } + }) + }) + + await t.test('should handle alternating roles correctly', () => { + const model = { id: 'deepseek-reasoner' } + const alternatingMessages = [ + { role: 'user', content: 'Q1' }, + { role: 'assistant', content: 'A1' }, + { role: 'user', content: 'Q2' }, + { role: 'assistant', content: 'A2' } + ] as ChatCompletionMessageParam[] + + const result = processReqMessages(model, alternatingMessages) + + assert.strictEqual(result.length, 4) + assert.deepStrictEqual(result, alternatingMessages) + }) + + await t.test('should handle messages with empty content', () => { + const model = { id: 'deepseek-reasoner' } + const messagesWithEmpty = [ + { role: 'user', content: 'Q1' }, + { role: 'user', content: '' }, + { role: 'user', content: 'Q2' } + ] as ChatCompletionMessageParam[] + + const result = processReqMessages(model, messagesWithEmpty) + + assert.strictEqual(result.length, 1) + assert.deepStrictEqual(result[0], { + role: 'user', + content: 'Q1\n\nQ2' + }) + }) +})