fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario (#5112)
* fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario. * fix: @ts-ignore
This commit is contained in:
parent
81eab1179b
commit
9bb96c212d
@ -408,6 +408,9 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
} as ChatCompletionMessageParam)
|
} as ChatCompletionMessageParam)
|
||||||
toolResults.forEach((ts) => reqMessages.push(ts as ChatCompletionMessageParam))
|
toolResults.forEach((ts) => reqMessages.push(ts as ChatCompletionMessageParam))
|
||||||
|
|
||||||
|
console.debug('[tool] reqMessages before processing', model.id, reqMessages)
|
||||||
|
reqMessages = processReqMessages(model, reqMessages)
|
||||||
|
console.debug('[tool] reqMessages', model.id, reqMessages)
|
||||||
const newStream = await this.sdk.chat.completions
|
const newStream = await this.sdk.chat.completions
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
.create(
|
.create(
|
||||||
@ -506,9 +509,9 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
await processToolUses(content, idx)
|
await processToolUses(content, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('[before] reqMessages', reqMessages)
|
console.debug('[completions] reqMessages before processing', model.id, reqMessages)
|
||||||
reqMessages = processReqMessages(model, reqMessages)
|
reqMessages = processReqMessages(model, reqMessages)
|
||||||
// console.log('[after] reqMessages', reqMessages)
|
console.debug('[completions] reqMessages', model.id, reqMessages)
|
||||||
const stream = await this.sdk.chat.completions
|
const stream = await this.sdk.chat.completions
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
.create(
|
.create(
|
||||||
@ -571,6 +574,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
await this.checkIsCopilot()
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
|
console.debug('[translate] reqMessages', model.id, messages)
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
const response = await this.sdk.chat.completions.create({
|
const response = await this.sdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
@ -646,6 +650,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
await this.checkIsCopilot()
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
|
console.debug('[summaries] reqMessages', model.id, [systemMessage, userMessage])
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
const response = await this.sdk.chat.completions.create({
|
const response = await this.sdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
@ -680,6 +685,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
content: messages.map((m) => m.content).join('\n')
|
content: messages.map((m) => m.content).join('\n')
|
||||||
}
|
}
|
||||||
|
console.debug('[summaryForSearch] reqMessages', model.id, [systemMessage, userMessage])
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
const response = await this.sdk.chat.completions.create(
|
const response = await this.sdk.chat.completions.create(
|
||||||
{
|
{
|
||||||
@ -771,6 +777,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.checkIsCopilot()
|
await this.checkIsCopilot()
|
||||||
|
console.debug('[checkModel] body', model.id, body)
|
||||||
const response = await this.sdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
const response = await this.sdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -9,40 +9,33 @@ export function processReqMessages(
|
|||||||
return reqMessages
|
return reqMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeSameRoleMessages(reqMessages)
|
return interleaveUserAndAssistantMessages(reqMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
function needStrictlyInterleaveUserAndAssistantMessages(model: Model) {
|
function needStrictlyInterleaveUserAndAssistantMessages(model: Model) {
|
||||||
return model.id === 'deepseek-reasoner'
|
return model.id === 'deepseek-reasoner'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function interleaveUserAndAssistantMessages(messages: ChatCompletionMessageParam[]): ChatCompletionMessageParam[] {
|
||||||
* Merge successive messages with the same role
|
if (!messages || messages.length === 0) {
|
||||||
*/
|
return []
|
||||||
function mergeSameRoleMessages(messages: ChatCompletionMessageParam[]): ChatCompletionMessageParam[] {
|
}
|
||||||
const split = '\n'
|
|
||||||
const processedMessages: ChatCompletionMessageParam[] = []
|
const processedMessages: ChatCompletionMessageParam[] = []
|
||||||
let currentGroup: ChatCompletionMessageParam[] = []
|
|
||||||
|
|
||||||
for (const message of messages) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
if (currentGroup.length === 0 || currentGroup[0].role === message.role) {
|
const currentMessage = { ...messages[i] }
|
||||||
currentGroup.push(message)
|
|
||||||
} else {
|
if (i > 0 && currentMessage.role === messages[i - 1].role) {
|
||||||
// merge the current group and add to processed messages
|
// insert an empty message with the opposite role in between
|
||||||
|
const emptyMessageRole = currentMessage.role === 'user' ? 'assistant' : 'user'
|
||||||
processedMessages.push({
|
processedMessages.push({
|
||||||
...currentGroup[0],
|
role: emptyMessageRole,
|
||||||
content: currentGroup.map((m) => m.content).join(split)
|
content: ''
|
||||||
})
|
})
|
||||||
currentGroup = [message]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process the last group
|
processedMessages.push(currentMessage)
|
||||||
if (currentGroup.length > 0) {
|
|
||||||
processedMessages.push({
|
|
||||||
...currentGroup[0],
|
|
||||||
content: currentGroup.map((m) => m.content).join(split)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedMessages
|
return processedMessages
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Model } from '@renderer/types'
|
import type { Model } from '@renderer/types'
|
||||||
import { ChatCompletionMessageParam } from 'openai/resources'
|
import { ChatCompletionMessageParam } from 'openai/resources'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
@ -14,38 +14,47 @@ describe('ModelMessageService', () => {
|
|||||||
{ role: 'assistant', content: 'Second answer' }
|
{ role: 'assistant', content: 'Second answer' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const createModel = (id: string): Model => ({
|
it('should insert empty messages between consecutive same-role messages for deepseek-reasoner model', () => {
|
||||||
id,
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
provider: 'test-provider',
|
|
||||||
name: id,
|
|
||||||
group: 'test-group'
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should merge successive messages with same role for deepseek-reasoner model', () => {
|
|
||||||
const model = createModel('deepseek-reasoner')
|
|
||||||
const result = processReqMessages(model, mockMessages)
|
const result = processReqMessages(model, mockMessages)
|
||||||
|
|
||||||
expect(result.length).toBe(4)
|
expect(result.length).toBe(8)
|
||||||
expect(result[0]).toEqual({
|
expect(result[0]).toEqual({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'First question\nAdditional context'
|
content: 'First question'
|
||||||
})
|
})
|
||||||
expect(result[1]).toEqual({
|
expect(result[1]).toEqual({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'First answer\nAdditional information'
|
content: ''
|
||||||
})
|
})
|
||||||
expect(result[2]).toEqual({
|
expect(result[2]).toEqual({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'Second question'
|
content: 'Additional context'
|
||||||
})
|
})
|
||||||
expect(result[3]).toEqual({
|
expect(result[3]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'First answer'
|
||||||
|
})
|
||||||
|
expect(result[4]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[5]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Additional information'
|
||||||
|
})
|
||||||
|
expect(result[6]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Second question'
|
||||||
|
})
|
||||||
|
expect(result[7]).toEqual({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'Second answer'
|
content: 'Second answer'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not merge messages for other models', () => {
|
it('should not modify messages for other models', () => {
|
||||||
const model = createModel('gpt-4')
|
const model = { id: 'gpt-4' } as Model
|
||||||
const result = processReqMessages(model, mockMessages)
|
const result = processReqMessages(model, mockMessages)
|
||||||
|
|
||||||
expect(result.length).toBe(mockMessages.length)
|
expect(result.length).toBe(mockMessages.length)
|
||||||
@ -53,7 +62,7 @@ describe('ModelMessageService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle empty messages array', () => {
|
it('should handle empty messages array', () => {
|
||||||
const model = createModel('deepseek-reasoner')
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
const result = processReqMessages(model, [])
|
const result = processReqMessages(model, [])
|
||||||
|
|
||||||
expect(result.length).toBe(0)
|
expect(result.length).toBe(0)
|
||||||
@ -61,16 +70,16 @@ describe('ModelMessageService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle single message', () => {
|
it('should handle single message', () => {
|
||||||
const model = createModel('deepseek-reasoner')
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
const singleMessage = [{ role: 'user', content: 'Single message' }]
|
const singleMessage: ChatCompletionMessageParam[] = [{ role: 'user', content: 'Single message' }]
|
||||||
const result = processReqMessages(model, singleMessage as ChatCompletionMessageParam[])
|
const result = processReqMessages(model, singleMessage)
|
||||||
|
|
||||||
expect(result.length).toBe(1)
|
expect(result.length).toBe(1)
|
||||||
expect(result).toEqual(singleMessage)
|
expect(result).toEqual(singleMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve other message properties when merging', () => {
|
it('should preserve other message properties when inserting empty messages', () => {
|
||||||
const model = createModel('deepseek-reasoner')
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
const messagesWithProps = [
|
const messagesWithProps = [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -87,17 +96,26 @@ describe('ModelMessageService', () => {
|
|||||||
|
|
||||||
const result = processReqMessages(model, messagesWithProps)
|
const result = processReqMessages(model, messagesWithProps)
|
||||||
|
|
||||||
expect(result.length).toBe(1)
|
expect(result.length).toBe(3)
|
||||||
expect(result[0]).toEqual({
|
expect(result[0]).toEqual({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'First message\nSecond message',
|
content: 'First message',
|
||||||
name: 'user1',
|
name: 'user1',
|
||||||
function_call: { name: 'test', arguments: '{}' }
|
function_call: { name: 'test', arguments: '{}' }
|
||||||
})
|
})
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[2]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Second message',
|
||||||
|
name: 'user1'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle alternating roles correctly', () => {
|
it('should handle alternating roles correctly', () => {
|
||||||
const model = createModel('deepseek-reasoner')
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
const alternatingMessages = [
|
const alternatingMessages = [
|
||||||
{ role: 'user', content: 'Q1' },
|
{ role: 'user', content: 'Q1' },
|
||||||
{ role: 'assistant', content: 'A1' },
|
{ role: 'assistant', content: 'A1' },
|
||||||
@ -112,7 +130,7 @@ describe('ModelMessageService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle messages with empty content', () => {
|
it('should handle messages with empty content', () => {
|
||||||
const model = createModel('deepseek-reasoner')
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
const messagesWithEmpty = [
|
const messagesWithEmpty = [
|
||||||
{ role: 'user', content: 'Q1' },
|
{ role: 'user', content: 'Q1' },
|
||||||
{ role: 'user', content: '' },
|
{ role: 'user', content: '' },
|
||||||
@ -121,10 +139,84 @@ describe('ModelMessageService', () => {
|
|||||||
|
|
||||||
const result = processReqMessages(model, messagesWithEmpty)
|
const result = processReqMessages(model, messagesWithEmpty)
|
||||||
|
|
||||||
expect(result.length).toBe(1)
|
expect(result.length).toBe(5)
|
||||||
expect(result[0]).toEqual({
|
expect(result[0]).toEqual({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'Q1\n\nQ2'
|
content: 'Q1'
|
||||||
|
})
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[2]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[3]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[4]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Q2'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle specific case with consecutive user messages', () => {
|
||||||
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
|
const messages = [
|
||||||
|
{ role: 'assistant', content: 'Initial assistant message' },
|
||||||
|
{ role: 'user', content: 'First user message' },
|
||||||
|
{ role: 'user', content: 'Second user message' }
|
||||||
|
] as ChatCompletionMessageParam[]
|
||||||
|
|
||||||
|
const result = processReqMessages(model, messages)
|
||||||
|
|
||||||
|
expect(result.length).toBe(4)
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Initial assistant message'
|
||||||
|
})
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'First user message'
|
||||||
|
})
|
||||||
|
expect(result[2]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[3]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Second user message'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle specific case with consecutive assistant messages', () => {
|
||||||
|
const model = { id: 'deepseek-reasoner' } as Model
|
||||||
|
const messages = [
|
||||||
|
{ role: 'user', content: 'Initial user message' },
|
||||||
|
{ role: 'assistant', content: 'First assistant message' },
|
||||||
|
{ role: 'assistant', content: 'Second assistant message' }
|
||||||
|
] as ChatCompletionMessageParam[]
|
||||||
|
|
||||||
|
const result = processReqMessages(model, messages)
|
||||||
|
|
||||||
|
expect(result.length).toBe(4)
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Initial user message'
|
||||||
|
})
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'First assistant message'
|
||||||
|
})
|
||||||
|
expect(result[2]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
expect(result[3]).toEqual({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Second assistant message'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user