mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
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:
parent
198c9e24be
commit
ee6e88664e
@ -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) {
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/images/models/gpt_image_1.png
Normal file
BIN
src/renderer/src/assets/images/models/gpt_image_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@ -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.*$')
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user