feat: Support reasoning control for Doubao/Mistral models. (#7116)

* feat: Support reasoning control for Doubao models.

* feat: Enhance model handling and support for Doubao and Gemini in API clients

- Added support for Doubao thinking modes in OpenAIAPIClient and GeminiAPIClient.
- Introduced GEMINI_FLASH_MODEL_REGEX for model identification.
- Updated models.ts to include new Doubao and Gemini model regex patterns.
- Added new image asset for ChatGPT in models.
- Enhanced reasoning control and token budget handling for Doubao models.
- Improved the Inputbar's ThinkingButton component to accommodate new thinking options.

---------

Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
Xin Rui 2025-06-13 00:03:58 +08:00 committed by GitHub
parent 198c9e24be
commit ee6e88664e
6 changed files with 97 additions and 13 deletions

View File

@ -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) {

View File

@ -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 {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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.*$')

View File

@ -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<string, ThinkingOption[]> = {
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<ThinkingOption, ThinkingOption> = {
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<Props> = ({ 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<Props> = ({ 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(() => {

View File

@ -47,7 +47,7 @@ export type RequestOptions = Anthropic.RequestOptions | OpenAI.RequestOptions |
type OpenAIParamsWithoutReasoningEffort = Omit<OpenAI.Chat.Completions.ChatCompletionCreateParams, 'reasoning_effort'>
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