diff --git a/package.json b/package.json index 2198986545..66b42c208b 100644 --- a/package.json +++ b/package.json @@ -270,7 +270,7 @@ "winston-daily-rotate-file": "^5.0.0", "word-extractor": "^1.0.4", "zipread": "^1.3.3", - "zod": "^3.25.74" + "zod": "^4.0.0" }, "resolutions": { "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 60173551b4..502756d5a1 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -52,6 +52,7 @@ import { import { ChunkType, TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk' import { Message } from '@renderer/types/newMessage' import { + MistralDeltaSchema, OpenAISdkMessageParam, OpenAISdkParams, OpenAISdkRawChunk, @@ -804,7 +805,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient< (typeof choice.delta.content === 'string' && choice.delta.content !== '') || (typeof (choice.delta as any).reasoning_content === 'string' && (choice.delta as any).reasoning_content !== '') || - (typeof (choice.delta as any).reasoning === 'string' && (choice.delta as any).reasoning !== '')) + (typeof (choice.delta as any).reasoning === 'string' && (choice.delta as any).reasoning !== '') || + Array.isArray(choice.delta.content)) ) { contentSource = choice.delta } else if ('message' in choice) { @@ -815,8 +817,15 @@ export class OpenAIAPIClient extends OpenAIBaseClient< if (!contentSource?.content) { accumulatingText = false } - // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it - if (!contentSource?.reasoning_content && !contentSource?.reasoning) { + const mistralDelta = MistralDeltaSchema.safeParse(contentSource?.content) + + if ( + // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it + !contentSource?.reasoning_content && + // @ts-ignore - reasoning is not in standard OpenAI types but some providers use it + !contentSource?.reasoning && + (mistralDelta.data?.[0]?.type !== 'thinking' || !mistralDelta.success) + ) { isThinking = false } @@ -850,12 +859,14 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } // 处理推理内容 (e.g. from OpenRouter DeepSeek-R1) - // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it - const reasoningText = contentSource.reasoning_content || contentSource.reasoning + const reasoningText = + // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it + contentSource.reasoning_content || + // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it + contentSource.reasoning || + (mistralDelta.data?.[0]?.type === 'thinking' ? mistralDelta.data?.[0]?.thinking[0]?.text : undefined) if (reasoningText) { - // logger.silly('since reasoningText is trusy, try to enqueue THINKING_START AND THINKING_DELTA') if (!isThinking) { - // logger.silly('since isThinking is falsy, try to enqueue THINKING_START') controller.enqueue({ type: ChunkType.THINKING_START } as ThinkingStartChunk) @@ -872,22 +883,35 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } // 处理文本内容 - if (contentSource.content) { - // logger.silly('since contentSource.content is trusy, try to enqueue TEXT_START and TEXT_DELTA') + if (mistralDelta.success && mistralDelta.data?.[0]?.type === 'text') { if (!accumulatingText) { - // logger.silly('enqueue TEXT_START') controller.enqueue({ type: ChunkType.TEXT_START } as TextStartChunk) accumulatingText = true } - // logger.silly('enqueue TEXT_DELTA') controller.enqueue({ type: ChunkType.TEXT_DELTA, - text: contentSource.content + text: mistralDelta.data?.[0]?.text }) - } else { - accumulatingText = false + } else if (!mistralDelta.success) { + if (contentSource.content) { + // logger.silly('since contentSource.content is trusy, try to enqueue TEXT_START and TEXT_DELTA') + if (!accumulatingText) { + // logger.silly('enqueue TEXT_START') + controller.enqueue({ + type: ChunkType.TEXT_START + } as TextStartChunk) + accumulatingText = true + } + // logger.silly('enqueue TEXT_DELTA') + controller.enqueue({ + type: ChunkType.TEXT_DELTA, + text: contentSource.content + }) + } else { + accumulatingText = false + } } // 处理工具调用 diff --git a/src/renderer/src/types/sdk.ts b/src/renderer/src/types/sdk.ts index a5413e54fb..a8a208523e 100644 --- a/src/renderer/src/types/sdk.ts +++ b/src/renderer/src/types/sdk.ts @@ -23,6 +23,7 @@ import { } from '@google/genai' import OpenAI, { AzureOpenAI } from 'openai' import { Stream } from 'openai/streaming' +import * as z from 'zod' import { EndpointType } from './index' @@ -260,3 +261,19 @@ export interface AwsBedrockSdkToolCall { input: any toolUseId: string } + +export const MistralDeltaTextSchema = z.object({ + type: z.literal('text'), + text: z.string() +}) + +export const MistralDeltaThinkingSchema = z.object({ + type: z.literal('thinking'), + thinking: z.array(MistralDeltaTextSchema) +}) + +export const MistralDeltaSchema = z.array( + z.discriminatedUnion('type', [MistralDeltaTextSchema, MistralDeltaThinkingSchema]) +) + +export type MistralDelta = z.infer diff --git a/yarn.lock b/yarn.lock index c970bca393..efcc401c00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8650,7 +8650,7 @@ __metadata: winston-daily-rotate-file: "npm:^5.0.0" word-extractor: "npm:^1.0.4" zipread: "npm:^1.3.3" - zod: "npm:^3.25.74" + zod: "npm:^4.0.0" languageName: unknown linkType: soft @@ -22669,10 +22669,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.25.74": - version: 3.25.74 - resolution: "zod@npm:3.25.74" - checksum: 10c0/59e38b046ac333b5bd1ba325a83b6798721227cbfb1e69dfc7159bd7824b904241ab923026edb714fafefec3624265ae374a70aee9a5a45b365bd31781ffa105 +"zod@npm:^4.0.0": + version: 4.0.17 + resolution: "zod@npm:4.0.17" + checksum: 10c0/c56ef4cc02f8f52be8724c5a8b338266202d68477c7606bee9b7299818b75c9adc27f16f4b6704a372f3e7578bd016f389de19bfec766564b7c39d0d327c540a languageName: node linkType: hard