diff --git a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts index b40aff9182..bc848df7f9 100644 --- a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts +++ b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts @@ -19,7 +19,13 @@ import { } from '@google/genai' import { nanoid } from '@reduxjs/toolkit' import { GenericChunk } from '@renderer/aiCore/middleware/schemas' -import { findTokenLimit, isGeminiReasoningModel, isGemmaModel, isVisionModel } from '@renderer/config/models' +import { + findTokenLimit, + GEMINI_FLASH_MODEL_REGEX, + isGeminiReasoningModel, + isGemmaModel, + isVisionModel +} from '@renderer/config/models' import { CacheService } from '@renderer/services/CacheService' import { estimateTextTokens } from '@renderer/services/TokenService' import { @@ -378,7 +384,6 @@ export class GeminiAPIClient extends BaseApiClient< private getBudgetToken(assistant: Assistant, model: Model) { if (isGeminiReasoningModel(model)) { const reasoningEffort = assistant?.settings?.reasoning_effort - const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$') // 如果thinking_budget是undefined,不思考 if (reasoningEffort === undefined) { diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 1a78aea0f8..07359c837f 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -2,12 +2,15 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant' import Logger from '@renderer/config/logger' import { findTokenLimit, + GEMINI_FLASH_MODEL_REGEX, getOpenAIWebSearchParams, + isDoubaoThinkingAutoModel, isReasoningModel, isSupportedReasoningEffortGrokModel, isSupportedReasoningEffortModel, isSupportedReasoningEffortOpenAIModel, isSupportedThinkingTokenClaudeModel, + isSupportedThinkingTokenDoubaoModel, isSupportedThinkingTokenGeminiModel, isSupportedThinkingTokenModel, isSupportedThinkingTokenQwenModel, @@ -92,6 +95,23 @@ export class OpenAIAPIClient extends OpenAIBaseClient< return {} } const reasoningEffort = assistant?.settings?.reasoning_effort + + // Doubao 思考模式支持 + if (isSupportedThinkingTokenDoubaoModel(model)) { + // reasoningEffort 为空,默认开启 enabled + if (!reasoningEffort) { + return { thinking: { type: 'disabled' } } + } + if (reasoningEffort === 'high') { + return { thinking: { type: 'enabled' } } + } + if (reasoningEffort === 'auto' && isDoubaoThinkingAutoModel(model)) { + return { thinking: { type: 'auto' } } + } + // 其他情况不带 thinking 字段 + return {} + } + if (!reasoningEffort) { if (isSupportedThinkingTokenQwenModel(model)) { return { enable_thinking: false } @@ -106,9 +126,14 @@ export class OpenAIAPIClient extends OpenAIBaseClient< if (this.provider.id === 'openrouter') { return { reasoning: { max_tokens: 0, exclude: true } } } - return { - reasoning_effort: 'none' + if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) { + return { reasoning_effort: 'none' } } + return {} + } + + if (isSupportedThinkingTokenDoubaoModel(model)) { + return { thinking: { type: 'disabled' } } } return {} @@ -164,6 +189,17 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } } + // Doubao models + if (isSupportedThinkingTokenDoubaoModel(model)) { + if (assistant.settings?.reasoning_effort === 'high') { + return { + thinking: { + type: 'enabled' + } + } + } + } + // Default case: no special thinking settings return {} } diff --git a/src/renderer/src/assets/images/models/gpt_image_1.png b/src/renderer/src/assets/images/models/gpt_image_1.png new file mode 100644 index 0000000000..30f2f2708f Binary files /dev/null and b/src/renderer/src/assets/images/models/gpt_image_1.png differ diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 361478e411..78f4ff3d0b 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -55,6 +55,7 @@ import { default as ChatGptModelLogoDakr, default as ChatGPTo1ModelLogoDark } from '@renderer/assets/images/models/gpt_dark.png' +import ChatGPTImageModelLogo from '@renderer/assets/images/models/gpt_image_1.png' import ChatGPTo1ModelLogo from '@renderer/assets/images/models/gpt_o1.png' import GrokModelLogo from '@renderer/assets/images/models/grok.png' import GrokModelLogoDark from '@renderer/assets/images/models/grok_dark.png' @@ -181,7 +182,8 @@ const visionAllowedModels = [ 'o4(?:-[\\w-]+)?', 'deepseek-vl(?:[\\w-]+)?', 'kimi-latest', - 'gemma-3(?:-[\\w-]+)' + 'gemma-3(?:-[\\w-]+)', + 'doubao-1.6-seed(?:-[\\w-]+)' ] const visionExcludedModels = [ @@ -291,6 +293,7 @@ export function getModelLogo(modelId: string) { o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, o4: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, + 'gpt-image': ChatGPTImageModelLogo, 'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark, 'gpt-4': isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, @@ -312,6 +315,7 @@ export function getModelLogo(modelId: string) { mistral: isLight ? MistralModelLogo : MistralModelLogoDark, codestral: CodestralModelLogo, ministral: isLight ? MistralModelLogo : MistralModelLogoDark, + magistral: isLight ? MistralModelLogo : MistralModelLogoDark, moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, kimi: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark, @@ -2411,7 +2415,8 @@ export function isSupportedThinkingTokenModel(model?: Model): boolean { return ( isSupportedThinkingTokenGeminiModel(model) || isSupportedThinkingTokenQwenModel(model) || - isSupportedThinkingTokenClaudeModel(model) + isSupportedThinkingTokenClaudeModel(model) || + isSupportedThinkingTokenDoubaoModel(model) ) } @@ -2493,6 +2498,14 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean { ) } +export function isSupportedThinkingTokenDoubaoModel(model?: Model): boolean { + if (!model) { + return false + } + + return DOUBAO_THINKING_MODEL_REGEX.test(model.id) +} + export function isClaudeReasoningModel(model?: Model): boolean { if (!model) { return false @@ -2513,7 +2526,12 @@ export function isReasoningModel(model?: Model): boolean { } if (model.provider === 'doubao') { - return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false + return ( + REASONING_REGEX.test(model.name) || + model.type?.includes('reasoning') || + isSupportedThinkingTokenDoubaoModel(model) || + false + ) } if ( @@ -2522,7 +2540,8 @@ export function isReasoningModel(model?: Model): boolean { isGeminiReasoningModel(model) || isQwenReasoningModel(model) || isGrokReasoningModel(model) || - model.id.includes('glm-z1') + model.id.includes('glm-z1') || + model.id.includes('magistral') ) { return true } @@ -2804,3 +2823,16 @@ export const findTokenLimit = (modelId: string): { min: number; max: number } | } return undefined } + +// Doubao 支持思考模式的模型正则 +export const DOUBAO_THINKING_MODEL_REGEX = + /doubao-(?:1(\.|-5)-thinking-vision-pro|1(\.|-)5-thinking-pro-m|seed-1\.6|seed-1\.6-flash)(?:-[\\w-]+)?/i + +// 支持 auto 的 Doubao 模型 +export const DOUBAO_THINKING_AUTO_MODEL_REGEX = /doubao-(?:1-5-thinking-pro-m|seed-1.6)(?:-[\\w-]+)?/i + +export function isDoubaoThinkingAutoModel(model: Model): boolean { + return DOUBAO_THINKING_AUTO_MODEL_REGEX.test(model.id) +} + +export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$') diff --git a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx index 2caef6c158..21db131cef 100644 --- a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx @@ -7,7 +7,9 @@ import { } from '@renderer/components/Icons/SVGIcon' import { useQuickPanel } from '@renderer/components/QuickPanel' import { + isDoubaoThinkingAutoModel, isSupportedReasoningEffortGrokModel, + isSupportedThinkingTokenDoubaoModel, isSupportedThinkingTokenGeminiModel, isSupportedThinkingTokenQwenModel } from '@renderer/config/models' @@ -35,13 +37,14 @@ const MODEL_SUPPORTED_OPTIONS: Record = { default: ['off', 'low', 'medium', 'high'], grok: ['off', 'low', 'high'], gemini: ['off', 'low', 'medium', 'high', 'auto'], - qwen: ['off', 'low', 'medium', 'high'] + qwen: ['off', 'low', 'medium', 'high'], + doubao: ['off', 'auto', 'high'] } // 选项转换映射表:当选项不支持时使用的替代选项 const OPTION_FALLBACK: Record = { off: 'off', - low: 'low', + low: 'high', medium: 'high', // medium -> high (for Grok models) high: 'high', auto: 'high' // auto -> high (for non-Gemini models) @@ -55,6 +58,7 @@ const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): Re const isGrokModel = isSupportedReasoningEffortGrokModel(model) const isGeminiModel = isSupportedThinkingTokenGeminiModel(model) const isQwenModel = isSupportedThinkingTokenQwenModel(model) + const isDoubaoModel = isSupportedThinkingTokenDoubaoModel(model) const currentReasoningEffort = useMemo(() => { return assistant.settings?.reasoning_effort || 'off' @@ -65,13 +69,20 @@ const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): Re if (isGeminiModel) return 'gemini' if (isGrokModel) return 'grok' if (isQwenModel) return 'qwen' + if (isDoubaoModel) return 'doubao' return 'default' - }, [isGeminiModel, isGrokModel, isQwenModel]) + }, [isGeminiModel, isGrokModel, isQwenModel, isDoubaoModel]) // 获取当前模型支持的选项 const supportedOptions = useMemo(() => { + if (modelType === 'doubao') { + if (isDoubaoThinkingAutoModel(model)) { + return ['off', 'auto', 'high'] as ThinkingOption[] + } + return ['off', 'high'] as ThinkingOption[] + } return MODEL_SUPPORTED_OPTIONS[modelType] - }, [modelType]) + }, [model, modelType]) // 检查当前设置是否与当前模型兼容 useEffect(() => { diff --git a/src/renderer/src/types/sdk.ts b/src/renderer/src/types/sdk.ts index cef04febff..c066952ec1 100644 --- a/src/renderer/src/types/sdk.ts +++ b/src/renderer/src/types/sdk.ts @@ -47,7 +47,7 @@ export type RequestOptions = Anthropic.RequestOptions | OpenAI.RequestOptions | type OpenAIParamsWithoutReasoningEffort = Omit export type ReasoningEffortOptionalParams = { - thinking?: { type: 'disabled' | 'enabled'; budget_tokens?: number } + thinking?: { type: 'disabled' | 'enabled' | 'auto'; budget_tokens?: number } reasoning?: { max_tokens?: number; exclude?: boolean; effort?: string } | OpenAI.Reasoning reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams['reasoning_effort'] | 'none' | 'auto' enable_thinking?: boolean