diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index b748e3c832..6211b70314 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -97,7 +97,8 @@ export default class ModernAiProvider { // 提前构建中间件 const middlewares = buildAiSdkMiddlewares({ ...config, - provider: this.actualProvider + provider: this.actualProvider, + assistant: config.assistant }) logger.debug('Built middlewares in completions', { middlewareCount: middlewares.length, diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index 924cc5f47e..54116949f1 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -1,12 +1,15 @@ import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins' import { loggerService } from '@logger' -import { type MCPTool, type Message, type Model, type Provider } from '@renderer/types' +import { isSupportedThinkingTokenQwenModel } from '@renderer/config/models' +import { isSupportEnableThinkingProvider } from '@renderer/config/providers' +import { type Assistant, MCPTool, type Message, type Model, type Provider } from '@renderer/types' import type { Chunk } from '@renderer/types/chunk' import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai' import { isOpenRouterGeminiGenerateImageModel } from '../utils/image' import { noThinkMiddleware } from './noThinkMiddleware' import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware' +import { qwenThinkingMiddleware } from './qwenThinkingMiddleware' import { toolChoiceMiddleware } from './toolChoiceMiddleware' const logger = loggerService.withContext('AiSdkMiddlewareBuilder') @@ -19,6 +22,7 @@ export interface AiSdkMiddlewareConfig { onChunk?: (chunk: Chunk) => void model?: Model provider?: Provider + assistant?: Assistant enableReasoning: boolean // 是否开启提示词工具调用 isPromptToolUse: boolean @@ -218,6 +222,21 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: function addModelSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: AiSdkMiddlewareConfig): void { if (!config.model || !config.provider) return + // Qwen models on providers that don't support enable_thinking parameter (like Ollama, LM Studio, NVIDIA) + // Use /think or /no_think suffix to control thinking mode + if ( + config.provider && + isSupportedThinkingTokenQwenModel(config.model) && + !isSupportEnableThinkingProvider(config.provider) + ) { + const enableThinking = config.assistant?.settings?.reasoning_effort !== undefined + builder.add({ + name: 'qwen-thinking-control', + middleware: qwenThinkingMiddleware(enableThinking) + }) + logger.debug(`Added Qwen thinking middleware with thinking ${enableThinking ? 'enabled' : 'disabled'}`) + } + // 可以根据模型ID或特性添加特定中间件 // 例如:图像生成模型、多模态模型等 if (isOpenRouterGeminiGenerateImageModel(config.model, config.provider)) { diff --git a/src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts b/src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts new file mode 100644 index 0000000000..34515a42c9 --- /dev/null +++ b/src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts @@ -0,0 +1,39 @@ +import { LanguageModelMiddleware } from 'ai' + +/** + * Qwen Thinking Middleware + * Controls thinking mode for Qwen models on providers that don't support enable_thinking parameter (like Ollama) + * Appends '/think' or '/no_think' suffix to user messages based on reasoning_effort setting + * @param enableThinking - Whether thinking mode is enabled (based on reasoning_effort !== undefined) + * @returns LanguageModelMiddleware + */ +export function qwenThinkingMiddleware(enableThinking: boolean): LanguageModelMiddleware { + const suffix = enableThinking ? ' /think' : ' /no_think' + + return { + middlewareVersion: 'v2', + + transformParams: async ({ params }) => { + const transformedParams = { ...params } + // Process messages in prompt + if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) { + transformedParams.prompt = transformedParams.prompt.map((message) => { + // Only process user messages + if (message.role === 'user') { + // Process content array + if (Array.isArray(message.content)) { + for (const part of message.content) { + if (part.type === 'text' && !part.text.endsWith('/think') && !part.text.endsWith('/no_think')) { + part.text += suffix + } + } + } + } + return message + }) + } + + return transformedParams + } + } +}