fix: preserve thinking block (#11901)

* fix: preserve thinking block

* test: add coverage for reasoning parts in assistant messages (#11902)

* Initial plan

* test: add test coverage for reasoning parts in assistant messages

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
SuYao 2025-12-14 20:05:45 +08:00 committed by GitHub
parent a1e44a6827
commit fd921103dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 2 deletions

View File

@ -109,6 +109,20 @@ const createImageBlock = (
...overrides ...overrides
}) })
const createThinkingBlock = (
messageId: string,
overrides: Partial<Omit<ThinkingMessageBlock, 'type' | 'messageId'>> = {}
): ThinkingMessageBlock => ({
id: overrides.id ?? `thinking-block-${++blockCounter}`,
messageId,
type: MessageBlockType.THINKING,
createdAt: overrides.createdAt ?? new Date(2024, 0, 1, 0, 0, blockCounter).toISOString(),
status: overrides.status ?? MessageBlockStatus.SUCCESS,
content: overrides.content ?? 'Let me think...',
thinking_millsec: overrides.thinking_millsec ?? 1000,
...overrides
})
describe('messageConverter', () => { describe('messageConverter', () => {
beforeEach(() => { beforeEach(() => {
convertFileBlockToFilePartMock.mockReset() convertFileBlockToFilePartMock.mockReset()
@ -229,6 +243,23 @@ describe('messageConverter', () => {
} }
]) ])
}) })
it('includes reasoning parts for assistant messages with thinking blocks', async () => {
const model = createModel()
const message = createMessage('assistant')
message.__mockContent = 'Here is my answer'
message.__mockThinkingBlocks = [createThinkingBlock(message.id, { content: 'Let me think...' })]
const result = await convertMessageToSdkParam(message, false, model)
expect(result).toEqual({
role: 'assistant',
content: [
{ type: 'text', text: 'Here is my answer' },
{ type: 'reasoning', text: 'Let me think...' }
]
})
})
}) })
describe('convertMessagesToSdkMessages', () => { describe('convertMessagesToSdkMessages', () => {

View File

@ -3,6 +3,7 @@
* Cherry Studio AI SDK * Cherry Studio AI SDK
*/ */
import type { ReasoningPart } from '@ai-sdk/provider-utils'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { isImageEnhancementModel, isVisionModel } from '@renderer/config/models' import { isImageEnhancementModel, isVisionModel } from '@renderer/config/models'
import type { Message, Model } from '@renderer/types' import type { Message, Model } from '@renderer/types'
@ -163,13 +164,13 @@ async function convertMessageToAssistantModelMessage(
thinkingBlocks: ThinkingMessageBlock[], thinkingBlocks: ThinkingMessageBlock[],
model?: Model model?: Model
): Promise<AssistantModelMessage> { ): Promise<AssistantModelMessage> {
const parts: Array<TextPart | FilePart> = [] const parts: Array<TextPart | ReasoningPart | FilePart> = []
if (content) { if (content) {
parts.push({ type: 'text', text: content }) parts.push({ type: 'text', text: content })
} }
for (const thinkingBlock of thinkingBlocks) { for (const thinkingBlock of thinkingBlocks) {
parts.push({ type: 'text', text: thinkingBlock.content }) parts.push({ type: 'reasoning', text: thinkingBlock.content })
} }
for (const fileBlock of fileBlocks) { for (const fileBlock of fileBlocks) {