mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
Feat/qwen3 support (#5533)
* feat: implement ThinkingPanel for managing reasoning effort and token limits - Added ThinkingPanel component to handle user settings for reasoning effort and thinking budget. - Introduced ThinkingSelect and ThinkingSlider components for selecting reasoning effort and adjusting token limits. - Updated models and hooks to support new reasoning effort and thinking budget features. - Enhanced Inputbar to integrate ThinkingPanel and provide a toggle for enabling thinking features. - Updated translations and styles for new components. * refactor: enhance ThinkingPanel and related components for improved reasoning effort management - Updated ThinkingPanel to streamline token mapping and error messaging. - Refactored ThinkingSelect to utilize a list for better UI interaction. - Enhanced ThinkingSlider with styled components for a more intuitive user experience. - Adjusted model checks in the configuration to support new reasoning models. - Improved translations for clarity and consistency across languages. * feat: close ThinkingPanel on outside click * feat(icons): add new lightbulb icons and update reasoning effort settings in translations - Introduced new SVG icons for lightbulb states (off, on at 10%, 50%, and 90%). - Added "Off" reasoning effort option in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese translations. - Refactored Inputbar and ThinkingButton components to integrate new reasoning effort logic and icon display. * feat(thinking): refactor reasoning effort management and introduce new lightbulb icon - Removed ThinkingPanel, ThinkingSelect, and ThinkingSlider components to streamline reasoning effort management. - Added new MdiLightbulbAutoOutline icon for the auto reasoning effort option. - Updated reasoning effort logic in ThinkingButton to support 'auto' and fallback options. - Enhanced translations for reasoning effort options across multiple languages. - Adjusted model configurations to integrate new reasoning effort settings. * refactor(messageThunk): update multi-model dispatch logic for improved assistant handling - Changed the parameter from baseAssistantId to assistant object for better clarity. - Updated tasksToQueue structure to include assistant configuration instead of just ID and model. - Enhanced the creation of assistant messages to utilize the full assistant object. * chore(messageThunk): remove unused comment for clarity in multi-model response handling * fix(ThinkingButton): update quick panel title for reasoning effort settings * fix(GeminiProvider, OpenAIProvider): apply Math.floor to budget calculations for improved accuracy --------- Co-authored-by: ousugo <dkzyxh@gmail.com> Co-authored-by: Teo <cheesen.xu@gmail.com>
This commit is contained in:
parent
7c0086a484
commit
ef7abbcb0e
@ -11,3 +11,58 @@ export const StreamlineGoodHealthAndWellBeing = (props: SVGProps<SVGSVGElement>)
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLightbulbOffOutline(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2C9.76 2 7.78 3.05 6.5 4.68l1.43 1.43C8.84 4.84 10.32 4 12 4a5 5 0 0 1 5 5c0 1.68-.84 3.16-2.11 4.06l1.42 1.44C17.94 13.21 19 11.24 19 9a7 7 0 0 0-7-7M3.28 4L2 5.27L5.04 8.3C5 8.53 5 8.76 5 9c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h5.73l4 4L20 20.72zm3.95 6.5l5.5 5.5H10v-2.42a5 5 0 0 1-2.77-3.08M9 20v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLightbulbAutoOutline(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 2c3.87 0 7 3.13 7 7c0 2.38-1.19 4.47-3 5.74V17c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2.26C3.19 13.47 2 11.38 2 9c0-3.87 3.13-7 7-7M6 21v-1h6v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1M9 4C6.24 4 4 6.24 4 9c0 2.05 1.23 3.81 3 4.58V16h4v-2.42c1.77-.77 3-2.53 3-4.58c0-2.76-2.24-5-5-5m10 9h-2l-3.2 9h1.9l.7-2h3.2l.7 2h1.9zm-2.15 5.65L18 15l1.15 3.65z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLightbulbOn10(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1 11h3v2H1zm18.1-7.5L17 5.6L18.4 7l2.1-2.1zM11 1h2v3h-2zM4.9 3.5L3.5 4.9L5.6 7L7 5.6zM10 22c0 .6.4 1 1 1h2c.6 0 1-.4 1-1v-1h-4zm2-16c-3.3 0-6 2.7-6 6c0 2.2 1.2 4.2 3 5.2V19c0 .6.4 1 1 1h4c.6 0 1-.4 1-1v-1.8c1.8-1 3-3 3-5.2c0-3.3-2.7-6-6-6m1 9.9V17h-2v-1.1c-1.7-.4-3-2-3-3.9c0-2.2 1.8-4 4-4s4 1.8 4 4c0 1.9-1.3 3.4-3 3.9m7-4.9h3v2h-3z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLightbulbOn50(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1 11h3v2H1zm9 11c0 .6.4 1 1 1h2c.6 0 1-.4 1-1v-1h-4zm3-21h-2v3h2zM4.9 3.5L3.5 4.9L5.6 7L7 5.6zM20 11v2h3v-2zm-.9-7.5L17 5.6L18.4 7l2.1-2.1zM18 12c0 2.2-1.2 4.2-3 5.2V19c0 .6-.4 1-1 1h-4c-.6 0-1-.4-1-1v-1.8c-1.8-1-3-3-3-5.2c0-3.3 2.7-6 6-6s6 2.7 6 6M8 12c0 .35.05.68.14 1h7.72c.09-.32.14-.65.14-1c0-2.21-1.79-4-4-4s-4 1.79-4 4"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLightbulbOn90(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 5.6L5.6 7L3.5 4.9l1.4-1.4zM10 22c0 .6.4 1 1 1h2c.6 0 1-.4 1-1v-1h-4zm-9-9h3v-2H1zM13 1h-2v3h2zm7 10v2h3v-2zm-.9-7.5L17 5.6L18.4 7l2.1-2.1zM18 12c0 2.2-1.2 4.2-3 5.2V19c0 .6-.4 1-1 1h-4c-.6 0-1-.4-1-1v-1.8c-1.8-1-3-3-3-5.2c0-3.3 2.7-6 6-6s6 2.7 6 6m-6-4c-1 0-1.91.38-2.61 1h5.22C13.91 8.38 13 8 12 8"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@ -210,6 +210,7 @@ export const FUNCTION_CALLING_MODELS = [
|
||||
'o1(?:-[\\w-]+)?',
|
||||
'claude',
|
||||
'qwen',
|
||||
'qwen3',
|
||||
'hunyuan',
|
||||
'deepseek',
|
||||
'glm-4(?:-[\\w-]+)?',
|
||||
@ -2218,30 +2219,40 @@ export function isVisionModel(model: Model): boolean {
|
||||
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
||||
}
|
||||
|
||||
export function isOpenAIoSeries(model: Model): boolean {
|
||||
export function isOpenAIReasoningModel(model: Model): boolean {
|
||||
return model.id.includes('o1') || model.id.includes('o3') || model.id.includes('o4')
|
||||
}
|
||||
|
||||
export function isSupportedReasoningEffortOpenAIModel(model: Model): boolean {
|
||||
return (
|
||||
(model.id.includes('o1') && !(model.id.includes('o1-preview') || model.id.includes('o1-mini'))) ||
|
||||
model.id.includes('o3') ||
|
||||
model.id.includes('o4')
|
||||
)
|
||||
}
|
||||
|
||||
export function isOpenAIWebSearch(model: Model): boolean {
|
||||
return model.id.includes('gpt-4o-search-preview') || model.id.includes('gpt-4o-mini-search-preview')
|
||||
}
|
||||
|
||||
export function isSupportedThinkingTokenModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
isSupportedThinkingTokenGeminiModel(model) ||
|
||||
isSupportedThinkingTokenQwenModel(model) ||
|
||||
isSupportedThinkingTokenClaudeModel(model)
|
||||
)
|
||||
}
|
||||
|
||||
export function isSupportedReasoningEffortModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
model.id.includes('claude-3-7-sonnet') ||
|
||||
model.id.includes('claude-3.7-sonnet') ||
|
||||
isOpenAIoSeries(model) ||
|
||||
isGrokReasoningModel(model) ||
|
||||
isGemini25ReasoningModel(model)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return isSupportedReasoningEffortOpenAIModel(model) || isSupportedReasoningEffortGrokModel(model)
|
||||
}
|
||||
|
||||
export function isGrokModel(model?: Model): boolean {
|
||||
@ -2263,7 +2274,9 @@ export function isGrokReasoningModel(model?: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export function isGemini25ReasoningModel(model?: Model): boolean {
|
||||
export const isSupportedReasoningEffortGrokModel = isGrokReasoningModel
|
||||
|
||||
export function isGeminiReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
@ -2275,6 +2288,51 @@ export function isGemini25ReasoningModel(model?: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export const isSupportedThinkingTokenGeminiModel = isGeminiReasoningModel
|
||||
|
||||
export function isQwenReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isSupportedThinkingTokenQwenModel(model)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (model.id.includes('qwq') || model.id.includes('qvq')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
model.id.includes('qwen3') ||
|
||||
[
|
||||
'qwen-plus-latest',
|
||||
'qwen-plus-0428',
|
||||
'qwen-plus-2025-04-28',
|
||||
'qwen-turbo-latest',
|
||||
'qwen-turbo-0428',
|
||||
'qwen-turbo-2025-04-28'
|
||||
].includes(model.id)
|
||||
)
|
||||
}
|
||||
|
||||
export function isClaudeReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
return model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet')
|
||||
}
|
||||
|
||||
export const isSupportedThinkingTokenClaudeModel = isClaudeReasoningModel
|
||||
|
||||
export function isReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
@ -2284,15 +2342,14 @@ export function isReasoningModel(model?: Model): boolean {
|
||||
return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false
|
||||
}
|
||||
|
||||
if (model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet') || isOpenAIoSeries(model)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isGemini25ReasoningModel(model)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (model.id.includes('glm-z1')) {
|
||||
if (
|
||||
isClaudeReasoningModel(model) ||
|
||||
isOpenAIReasoningModel(model) ||
|
||||
isGeminiReasoningModel(model) ||
|
||||
isQwenReasoningModel(model) ||
|
||||
isGrokReasoningModel(model) ||
|
||||
model.id.includes('glm-z1')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2487,3 +2544,27 @@ export function groupQwenModels(models: Model[]): Record<string, Model[]> {
|
||||
{} as Record<string, Model[]>
|
||||
)
|
||||
}
|
||||
|
||||
export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
// Gemini models
|
||||
'gemini-.*$': { min: 0, max: 24576 },
|
||||
|
||||
// Qwen models
|
||||
'qwen-plus-.*$': { min: 0, max: 38912 },
|
||||
'qwen-turbo-.*$': { min: 0, max: 38912 },
|
||||
'qwen3-0\\.6b$': { min: 0, max: 30720 },
|
||||
'qwen3-1\\.7b$': { min: 0, max: 30720 },
|
||||
'qwen3-.*$': { min: 0, max: 38912 },
|
||||
|
||||
// Claude models
|
||||
'claude-3[.-]7.*sonnet.*$': { min: 0, max: 64000 }
|
||||
}
|
||||
|
||||
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
|
||||
for (const [pattern, limits] of Object.entries(THINKING_TOKEN_MAP)) {
|
||||
if (new RegExp(pattern).test(modelId)) {
|
||||
return limits
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"settings.reasoning_effort": "Reasoning effort",
|
||||
"settings.reasoning_effort.high": "high",
|
||||
"settings.reasoning_effort.low": "low",
|
||||
"settings.reasoning_effort.medium": "medium",
|
||||
"settings.reasoning_effort.off": "off",
|
||||
"settings.reasoning_effort.tip": "Only supported by OpenAI o-series, Anthropic, and Grok reasoning models",
|
||||
"settings.reasoning_effort.off": "Off",
|
||||
"settings.reasoning_effort.high": "Think harder",
|
||||
"settings.reasoning_effort.low": "Think less",
|
||||
"settings.reasoning_effort.medium": "Think normally",
|
||||
"settings.reasoning_effort.default": "Default",
|
||||
"settings.more": "Assistant Settings"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +250,14 @@
|
||||
"input.upload.upload_from_local": "Upload local file...",
|
||||
"input.web_search.builtin": "Model Built-in",
|
||||
"input.web_search.builtin.enabled_content": "Use the built-in web search function of the model",
|
||||
"input.web_search.builtin.disabled_content": "The current model does not support web search"
|
||||
"input.web_search.builtin.disabled_content": "The current model does not support web search",
|
||||
"input.thinking": "Thinking",
|
||||
"input.thinking.mode.default": "Default",
|
||||
"input.thinking.mode.default.tip": "The model will automatically determine the number of tokens to think",
|
||||
"input.thinking.mode.custom": "Custom",
|
||||
"input.thinking.mode.custom.tip": "The maximum number of tokens the model can think. Need to consider the context limit of the model, otherwise an error will be reported",
|
||||
"input.thinking.mode.tokens.tip": "Set the number of thinking tokens to use.",
|
||||
"input.thinking.budget_exceeds_max": "Thinking budget exceeds the maximum token number"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Collapse",
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
"settings.preset_messages": "プリセットメッセージ",
|
||||
"settings.prompt": "プロンプト設定",
|
||||
"settings.reasoning_effort": "思考連鎖の長さ",
|
||||
"settings.reasoning_effort.high": "長い",
|
||||
"settings.reasoning_effort.low": "短い",
|
||||
"settings.reasoning_effort.medium": "中程度",
|
||||
"settings.reasoning_effort.off": "オフ",
|
||||
"settings.reasoning_effort.tip": "OpenAI o-series、Anthropic、および Grok の推論モデルのみサポート",
|
||||
"settings.reasoning_effort.high": "最大限の思考",
|
||||
"settings.reasoning_effort.low": "少しの思考",
|
||||
"settings.reasoning_effort.medium": "普通の思考",
|
||||
"settings.reasoning_effort.default": "デフォルト",
|
||||
"settings.more": "アシスタント設定"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +250,14 @@
|
||||
"input.upload.upload_from_local": "ローカルファイルをアップロード...",
|
||||
"input.web_search.builtin": "モデル内蔵",
|
||||
"input.web_search.builtin.enabled_content": "モデル内蔵のウェブ検索機能を使用",
|
||||
"input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません"
|
||||
"input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません",
|
||||
"input.thinking": "思考",
|
||||
"input.thinking.mode.default": "デフォルト",
|
||||
"input.thinking.mode.custom": "カスタム",
|
||||
"input.thinking.mode.custom.tip": "モデルが最大で思考できるトークン数。モデルのコンテキスト制限を考慮する必要があります。そうしないとエラーが発生します",
|
||||
"input.thinking.mode.default.tip": "モデルが自動的に思考のトークン数を決定します",
|
||||
"input.thinking.mode.tokens.tip": "思考のトークン数を設定します",
|
||||
"input.thinking.budget_exceeds_max": "思考予算が最大トークン数を超えました"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折りたたむ",
|
||||
@ -1508,7 +1515,7 @@
|
||||
"title": "プライバシー設定",
|
||||
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
|
||||
},
|
||||
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
|
||||
"input.show_translate_confirm": "翻訳確認ダイアログを表示"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意の言語",
|
||||
|
||||
@ -55,12 +55,11 @@
|
||||
"settings.model": "Настройки модели",
|
||||
"settings.preset_messages": "Предустановленные сообщения",
|
||||
"settings.prompt": "Настройки промптов",
|
||||
"settings.reasoning_effort": "Длина цепочки рассуждений",
|
||||
"settings.reasoning_effort.high": "Длинная",
|
||||
"settings.reasoning_effort.low": "Короткая",
|
||||
"settings.reasoning_effort.medium": "Средняя",
|
||||
"settings.reasoning_effort.off": "Выключено",
|
||||
"settings.reasoning_effort.tip": "Поддерживается только моделями рассуждений OpenAI o-series, Anthropic и Grok",
|
||||
"settings.reasoning_effort.off": "Выключить",
|
||||
"settings.reasoning_effort.high": "Стараюсь думать",
|
||||
"settings.reasoning_effort.low": "Меньше думать",
|
||||
"settings.reasoning_effort.medium": "Среднее",
|
||||
"settings.reasoning_effort.default": "По умолчанию",
|
||||
"settings.more": "Настройки ассистента"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +249,14 @@
|
||||
"input.upload.upload_from_local": "Загрузить локальный файл...",
|
||||
"input.web_search.builtin": "Модель встроена",
|
||||
"input.web_search.builtin.enabled_content": "Используйте встроенную функцию веб-поиска модели",
|
||||
"input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск"
|
||||
"input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск",
|
||||
"input.thinking": "Мыслим",
|
||||
"input.thinking.mode.default": "По умолчанию",
|
||||
"input.thinking.mode.default.tip": "Модель автоматически определяет количество токенов для размышления",
|
||||
"input.thinking.mode.custom": "Пользовательский",
|
||||
"input.thinking.mode.custom.tip": "Модель может максимально размышлять количество токенов. Необходимо учитывать ограничение контекста модели, иначе будет ошибка",
|
||||
"input.thinking.mode.tokens.tip": "Установите количество токенов для размышления",
|
||||
"input.thinking.budget_exceeds_max": "Бюджет размышления превышает максимальное количество токенов"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Свернуть",
|
||||
@ -1508,7 +1514,7 @@
|
||||
"title": "Настройки приватности",
|
||||
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
|
||||
},
|
||||
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
|
||||
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
"settings.preset_messages": "预设消息",
|
||||
"settings.prompt": "提示词设置",
|
||||
"settings.reasoning_effort": "思维链长度",
|
||||
"settings.reasoning_effort.high": "长",
|
||||
"settings.reasoning_effort.low": "短",
|
||||
"settings.reasoning_effort.medium": "中",
|
||||
"settings.reasoning_effort.off": "关",
|
||||
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型",
|
||||
"settings.reasoning_effort.off": "关闭",
|
||||
"settings.reasoning_effort.low": "浮想",
|
||||
"settings.reasoning_effort.medium": "斟酌",
|
||||
"settings.reasoning_effort.high": "沉思",
|
||||
"settings.reasoning_effort.default": "默认",
|
||||
"settings.more": "助手设置"
|
||||
},
|
||||
"auth": {
|
||||
@ -133,6 +133,13 @@
|
||||
"input.translating": "翻译中...",
|
||||
"input.send": "发送",
|
||||
"input.settings": "设置",
|
||||
"input.thinking": "思考",
|
||||
"input.thinking.mode.default": "默认",
|
||||
"input.thinking.mode.default.tip": "模型会自动确定思考的 token 数",
|
||||
"input.thinking.mode.custom": "自定义",
|
||||
"input.thinking.mode.custom.tip": "模型最多可以思考的 token 数。需要考虑模型的上下文限制,否则会报错",
|
||||
"input.thinking.mode.tokens.tip": "设置思考的 token 数",
|
||||
"input.thinking.budget_exceeds_max": "思考预算超过最大 token 数",
|
||||
"input.topics": " 话题 ",
|
||||
"input.translate": "翻译成{{target_language}}",
|
||||
"input.upload": "上传图片或文档",
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
"settings.preset_messages": "預設訊息",
|
||||
"settings.prompt": "提示詞設定",
|
||||
"settings.reasoning_effort": "思維鏈長度",
|
||||
"settings.reasoning_effort.high": "長",
|
||||
"settings.reasoning_effort.low": "短",
|
||||
"settings.reasoning_effort.medium": "中",
|
||||
"settings.reasoning_effort.off": "關",
|
||||
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series、Anthropic 和 Grok 推理模型",
|
||||
"settings.reasoning_effort.off": "關閉",
|
||||
"settings.reasoning_effort.high": "盡力思考",
|
||||
"settings.reasoning_effort.low": "稍微思考",
|
||||
"settings.reasoning_effort.medium": "正常思考",
|
||||
"settings.reasoning_effort.default": "預設",
|
||||
"settings.more": "助手設定"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +250,14 @@
|
||||
"input.upload.upload_from_local": "上傳本地文件...",
|
||||
"input.web_search.builtin": "模型內置",
|
||||
"input.web_search.builtin.enabled_content": "使用模型內置的網路搜尋功能",
|
||||
"input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能"
|
||||
"input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能",
|
||||
"input.thinking": "思考",
|
||||
"input.thinking.mode.default": "預設",
|
||||
"input.thinking.mode.default.tip": "模型會自動確定思考的 token 數",
|
||||
"input.thinking.mode.custom": "自定義",
|
||||
"input.thinking.mode.custom.tip": "模型最多可以思考的 token 數。需要考慮模型的上下文限制,否則會報錯",
|
||||
"input.thinking.mode.tokens.tip": "設置思考的 token 數",
|
||||
"input.thinking.budget_exceeds_max": "思考預算超過最大 token 數"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折疊",
|
||||
|
||||
1
src/renderer/src/locales/zh/translation.json
Normal file
1
src/renderer/src/locales/zh/translation.json
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { HolderOutlined } from '@ant-design/icons'
|
||||
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { isGenerateImageModel, isReasoningModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
@ -65,6 +65,7 @@ import MentionModelsInput from './MentionModelsInput'
|
||||
import NewContextButton from './NewContextButton'
|
||||
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
|
||||
import TokenCount from './TokenCount'
|
||||
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
|
||||
|
||||
@ -130,7 +131,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const knowledgeBaseButtonRef = useRef<KnowledgeBaseButtonRef>(null)
|
||||
const mcpToolsButtonRef = useRef<MCPToolsButtonRef>(null)
|
||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||
const webSearchButtonRef = useRef<WebSearchButtonRef>(null)
|
||||
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
|
||||
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedEstimate = useCallback(
|
||||
@ -185,6 +187,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||
|
||||
const baseUserMessage: MessageInputBaseParams = { assistant, topic, content: text }
|
||||
console.log('baseUserMessage', baseUserMessage)
|
||||
|
||||
// getUserMessage()
|
||||
if (uploadedFiles) {
|
||||
@ -919,6 +922,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
{isReasoningModel(model) && (
|
||||
<ThinkingButton
|
||||
ref={thinkingButtonRef}
|
||||
model={model}
|
||||
assistant={assistant}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
)}
|
||||
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||
{showKnowledgeIcon && (
|
||||
<KnowledgeBaseButton
|
||||
@ -1104,7 +1115,8 @@ const ToolbarButton = styled(Button)`
|
||||
&.active {
|
||||
background-color: var(--color-primary) !important;
|
||||
.anticon,
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.chevron-icon {
|
||||
color: var(--color-white-soft);
|
||||
}
|
||||
&:hover {
|
||||
|
||||
171
src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx
Normal file
171
src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import {
|
||||
MdiLightbulbAutoOutline,
|
||||
MdiLightbulbOffOutline,
|
||||
MdiLightbulbOn10,
|
||||
MdiLightbulbOn50,
|
||||
MdiLightbulbOn90
|
||||
} from '@renderer/components/Icons/SVGIcon'
|
||||
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { isSupportedReasoningEffortGrokModel, isSupportedThinkingTokenGeminiModel } from '@renderer/config/models'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Assistant, Model, ReasoningEffortOptions } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC, ReactElement, useCallback, useEffect, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ThinkingOption = ReasoningEffortOptions | 'off'
|
||||
|
||||
export interface ThinkingButtonRef {
|
||||
openQuickPanel: () => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ref?: React.RefObject<ThinkingButtonRef | null>
|
||||
model: Model
|
||||
assistant: Assistant
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
// 模型类型到支持选项的映射表
|
||||
const MODEL_SUPPORTED_OPTIONS: Record<string, ThinkingOption[]> = {
|
||||
default: ['off', 'low', 'medium', 'high'],
|
||||
grok: ['off', 'low', 'high'],
|
||||
gemini: ['off', 'low', 'medium', 'high', 'auto']
|
||||
}
|
||||
|
||||
// 选项转换映射表:当选项不支持时使用的替代选项
|
||||
const OPTION_FALLBACK: Record<ThinkingOption, ThinkingOption> = {
|
||||
off: 'off',
|
||||
low: 'low',
|
||||
medium: 'high', // medium -> high (for Grok models)
|
||||
high: 'high',
|
||||
auto: 'high' // auto -> high (for non-Gemini models)
|
||||
}
|
||||
|
||||
const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): ReactElement => {
|
||||
const { t } = useTranslation()
|
||||
const quickPanel = useQuickPanel()
|
||||
const { updateAssistantSettings } = useAssistant(assistant.id)
|
||||
|
||||
const isGrokModel = isSupportedReasoningEffortGrokModel(model)
|
||||
const isGeminiModel = isSupportedThinkingTokenGeminiModel(model)
|
||||
|
||||
const currentReasoningEffort = useMemo(() => {
|
||||
return assistant.settings?.reasoning_effort || 'off'
|
||||
}, [assistant.settings?.reasoning_effort])
|
||||
|
||||
// 确定当前模型支持的选项类型
|
||||
const modelType = useMemo(() => {
|
||||
if (isGeminiModel) return 'gemini'
|
||||
if (isGrokModel) return 'grok'
|
||||
return 'default'
|
||||
}, [isGeminiModel, isGrokModel])
|
||||
|
||||
// 获取当前模型支持的选项
|
||||
const supportedOptions = useMemo(() => {
|
||||
return MODEL_SUPPORTED_OPTIONS[modelType]
|
||||
}, [modelType])
|
||||
|
||||
// 检查当前设置是否与当前模型兼容
|
||||
useEffect(() => {
|
||||
if (currentReasoningEffort && !supportedOptions.includes(currentReasoningEffort)) {
|
||||
// 使用表中定义的替代选项
|
||||
const fallbackOption = OPTION_FALLBACK[currentReasoningEffort as ThinkingOption]
|
||||
|
||||
updateAssistantSettings({
|
||||
reasoning_effort: fallbackOption === 'off' ? undefined : fallbackOption
|
||||
})
|
||||
}
|
||||
}, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id])
|
||||
|
||||
const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => {
|
||||
const iconColor = isActive ? 'var(--color-link)' : 'var(--color-icon)'
|
||||
|
||||
switch (true) {
|
||||
case option === 'low':
|
||||
return <MdiLightbulbOn10 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
|
||||
case option === 'medium':
|
||||
return <MdiLightbulbOn50 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
|
||||
case option === 'high':
|
||||
return <MdiLightbulbOn90 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
|
||||
case option === 'auto':
|
||||
return <MdiLightbulbAutoOutline width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
|
||||
case option === 'off':
|
||||
return <MdiLightbulbOffOutline width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
|
||||
default:
|
||||
return <MdiLightbulbOffOutline width={18} height={18} style={{ color: iconColor }} />
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onThinkingChange = useCallback(
|
||||
(option?: ThinkingOption) => {
|
||||
const isEnabled = option !== undefined && option !== 'off'
|
||||
// 然后更新设置
|
||||
if (!isEnabled) {
|
||||
updateAssistantSettings({
|
||||
reasoning_effort: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
updateAssistantSettings({
|
||||
reasoning_effort: option
|
||||
})
|
||||
return
|
||||
},
|
||||
[updateAssistantSettings]
|
||||
)
|
||||
|
||||
const baseOptions = useMemo(() => {
|
||||
// 使用表中定义的选项创建UI选项
|
||||
return supportedOptions.map((option) => ({
|
||||
level: option,
|
||||
label: t(`assistants.settings.reasoning_effort.${option === 'auto' ? 'default' : option}`),
|
||||
description: '',
|
||||
icon: createThinkingIcon(option),
|
||||
isSelected: currentReasoningEffort === option,
|
||||
action: () => onThinkingChange(option)
|
||||
}))
|
||||
}, [t, createThinkingIcon, currentReasoningEffort, supportedOptions, onThinkingChange])
|
||||
|
||||
const panelItems = baseOptions
|
||||
|
||||
const openQuickPanel = useCallback(() => {
|
||||
quickPanel.open({
|
||||
title: t('assistants.settings.reasoning_effort'),
|
||||
list: panelItems,
|
||||
symbol: 'thinking'
|
||||
})
|
||||
}, [quickPanel, panelItems, t])
|
||||
|
||||
const handleOpenQuickPanel = useCallback(() => {
|
||||
if (quickPanel.isVisible && quickPanel.symbol === 'thinking') {
|
||||
quickPanel.close()
|
||||
} else {
|
||||
openQuickPanel()
|
||||
}
|
||||
}, [openQuickPanel, quickPanel])
|
||||
|
||||
// 获取当前应显示的图标
|
||||
const getThinkingIcon = useCallback(() => {
|
||||
// 如果当前选项不支持,显示回退选项的图标
|
||||
if (currentReasoningEffort && !supportedOptions.includes(currentReasoningEffort)) {
|
||||
const fallbackOption = OPTION_FALLBACK[currentReasoningEffort as ThinkingOption]
|
||||
return createThinkingIcon(fallbackOption, true)
|
||||
}
|
||||
return createThinkingIcon(currentReasoningEffort, currentReasoningEffort !== 'off')
|
||||
}, [createThinkingIcon, currentReasoningEffort, supportedOptions])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openQuickPanel
|
||||
}))
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('assistants.settings.reasoning_effort')} arrow>
|
||||
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
|
||||
{getThinkingIcon()}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThinkingButton
|
||||
@ -8,13 +8,11 @@ import {
|
||||
isMac,
|
||||
isWindows
|
||||
} from '@renderer/config/constant'
|
||||
import { isGrokReasoningModel, isSupportedReasoningEffortModel } from '@renderer/config/models'
|
||||
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
SendMessageShortcut,
|
||||
@ -52,9 +50,9 @@ import {
|
||||
TranslateLanguageVarious
|
||||
} from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { Button, Col, InputNumber, Row, Segmented, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -72,7 +70,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
@ -127,17 +124,9 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onReasoningEffortChange = useCallback(
|
||||
(value?: 'low' | 'medium' | 'high') => {
|
||||
updateAssistantSettings({ reasoning_effort: value })
|
||||
},
|
||||
[updateAssistantSettings]
|
||||
)
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setContextCount(DEFAULT_CONTEXTCOUNT)
|
||||
setReasoningEffort(undefined)
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
settings: {
|
||||
@ -148,7 +137,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
streamOutput: true,
|
||||
hideMessages: false,
|
||||
reasoning_effort: undefined,
|
||||
customParameters: []
|
||||
}
|
||||
})
|
||||
@ -160,25 +148,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
|
||||
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
||||
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
||||
setReasoningEffort(assistant?.settings?.reasoning_effort)
|
||||
}, [assistant])
|
||||
|
||||
useEffect(() => {
|
||||
// 当是Grok模型时,处理reasoning_effort的设置
|
||||
// For Grok models, only 'low' and 'high' reasoning efforts are supported.
|
||||
// This ensures compatibility with the model's capabilities and avoids unsupported configurations.
|
||||
if (isGrokReasoningModel(assistant?.model || getDefaultModel())) {
|
||||
const currentEffort = assistant?.settings?.reasoning_effort
|
||||
if (!currentEffort || currentEffort === 'low') {
|
||||
setReasoningEffort('low') // Default to 'low' if no effort is set or if it's already 'low'.
|
||||
onReasoningEffortChange('low')
|
||||
} else if (currentEffort === 'medium' || currentEffort === 'high') {
|
||||
setReasoningEffort('high') // Force 'high' for 'medium' or 'high' to simplify the configuration.
|
||||
onReasoningEffortChange('high')
|
||||
}
|
||||
}
|
||||
}, [assistant?.model, assistant?.settings?.reasoning_effort, onReasoningEffortChange])
|
||||
|
||||
const formatSliderTooltip = (value?: number) => {
|
||||
if (value === undefined) return ''
|
||||
return value === 20 ? '∞' : value.toString()
|
||||
@ -294,46 +265,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<Row align="middle">
|
||||
<Label>{t('assistants.settings.reasoning_effort')}</Label>
|
||||
<Tooltip title={t('assistants.settings.reasoning_effort.tip')}>
|
||||
<CircleHelp size={14} color="var(--color-text-2)" />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={24}>
|
||||
<SegmentedContainer>
|
||||
<Segmented
|
||||
value={reasoningEffort || 'off'}
|
||||
onChange={(value) => {
|
||||
const typedValue = value === 'off' ? undefined : (value as 'low' | 'medium' | 'high')
|
||||
setReasoningEffort(typedValue)
|
||||
onReasoningEffortChange(typedValue)
|
||||
}}
|
||||
options={
|
||||
isGrokReasoningModel(assistant?.model || getDefaultModel())
|
||||
? [
|
||||
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
|
||||
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') }
|
||||
]
|
||||
: [
|
||||
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
|
||||
{ value: 'medium', label: t('assistants.settings.reasoning_effort.medium') },
|
||||
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') },
|
||||
{ value: 'off', label: t('assistants.settings.reasoning_effort.off') }
|
||||
]
|
||||
}
|
||||
name="group"
|
||||
block
|
||||
/>
|
||||
</SegmentedContainer>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
<SettingGroup>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
||||
@ -706,27 +637,6 @@ export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
// Define the styled component with hover state styling
|
||||
const SegmentedContainer = styled.div`
|
||||
margin-top: 5px;
|
||||
.ant-segmented-item {
|
||||
font-size: 12px;
|
||||
}
|
||||
.ant-segmented-item-selected {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.ant-segmented-item:hover:not(.ant-segmented-item-selected) {
|
||||
background-color: var(--color-primary-bg) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.ant-segmented-thumb {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
.ant-select-selector {
|
||||
border-radius: 15px !important;
|
||||
|
||||
@ -6,7 +6,7 @@ import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/cons
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { Button, Col, Divider, Input, InputNumber, Radio, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { isNull } from 'lodash'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -23,7 +23,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort)
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
||||
@ -43,10 +42,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
}
|
||||
}
|
||||
|
||||
const onReasoningEffortChange = (value) => {
|
||||
updateAssistantSettings({ reasoning_effort: value })
|
||||
}
|
||||
|
||||
const onContextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
updateAssistantSettings({ contextCount: value })
|
||||
@ -153,7 +148,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
setMaxTokens(0)
|
||||
setStreamOutput(true)
|
||||
setTopP(1)
|
||||
setReasoningEffort(undefined)
|
||||
setCustomParameters([])
|
||||
updateAssistantSettings({
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
@ -162,7 +156,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
maxTokens: 0,
|
||||
streamOutput: true,
|
||||
topP: 1,
|
||||
reasoning_effort: undefined,
|
||||
customParameters: []
|
||||
})
|
||||
}
|
||||
@ -383,27 +376,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
/>
|
||||
</SettingRow>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<Label>
|
||||
{t('assistants.settings.reasoning_effort')}{' '}
|
||||
<Tooltip title={t('assistants.settings.reasoning_effort.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
<Radio.Group
|
||||
value={reasoningEffort}
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => {
|
||||
setReasoningEffort(e.target.value)
|
||||
onReasoningEffortChange(e.target.value)
|
||||
}}>
|
||||
<Radio.Button value="low">{t('assistants.settings.reasoning_effort.low')}</Radio.Button>
|
||||
<Radio.Button value="medium">{t('assistants.settings.reasoning_effort.medium')}</Radio.Button>
|
||||
<Radio.Button value="high">{t('assistants.settings.reasoning_effort.high')}</Radio.Button>
|
||||
<Radio.Button value={undefined}>{t('assistants.settings.reasoning_effort.off')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</SettingRow>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<Label>{t('models.custom_parameters')}</Label>
|
||||
<Button icon={<PlusOutlined />} onClick={onAddCustomParameter}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam, TextBlockParam } from '@anthropic-ai/sdk/resources'
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { isReasoningModel, isVisionModel } from '@renderer/config/models'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
@ -10,7 +10,7 @@ import {
|
||||
filterEmptyMessages,
|
||||
filterUserRoleStartMessages
|
||||
} from '@renderer/services/MessagesService'
|
||||
import { Assistant, FileTypes, MCPToolResponse, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { Assistant, EFFORT_RATIO, FileTypes, MCPToolResponse, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { ChunkType } from '@renderer/types/chunk'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { removeSpecialCharactersForTopicName } from '@renderer/utils'
|
||||
@ -23,8 +23,6 @@ import OpenAI from 'openai'
|
||||
import { CompletionsParams } from '.'
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
type ReasoningEffort = 'high' | 'medium' | 'low'
|
||||
|
||||
interface ReasoningConfig {
|
||||
type: 'enabled' | 'disabled'
|
||||
budget_tokens?: number
|
||||
@ -124,32 +122,23 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
* @param model - The model
|
||||
* @returns The reasoning effort
|
||||
*/
|
||||
private getReasoningEffort(assistant: Assistant, model: Model): ReasoningConfig | undefined {
|
||||
private getBudgetToken(assistant: Assistant, model: Model): ReasoningConfig | undefined {
|
||||
if (!isReasoningModel(model)) {
|
||||
return undefined
|
||||
}
|
||||
const { maxTokens } = getAssistantSettings(assistant)
|
||||
|
||||
const effortRatios: Record<ReasoningEffort, number> = {
|
||||
high: 0.8,
|
||||
medium: 0.5,
|
||||
low: 0.2
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (reasoningEffort === undefined) {
|
||||
return {
|
||||
type: 'disabled'
|
||||
}
|
||||
}
|
||||
|
||||
const effort = assistant?.settings?.reasoning_effort as ReasoningEffort
|
||||
const effortRatio = effortRatios[effort]
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
|
||||
if (!effortRatio) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const isClaude37Sonnet = model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet')
|
||||
|
||||
if (!isClaude37Sonnet) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const maxTokens = assistant?.settings?.maxTokens || DEFAULT_MAX_TOKENS
|
||||
const budgetTokens = Math.trunc(Math.max(Math.min(maxTokens * effortRatio, 32000), 1024))
|
||||
const budgetTokens = Math.floor((maxTokens || DEFAULT_MAX_TOKENS) * effortRatio * 0.8)
|
||||
|
||||
return {
|
||||
type: 'enabled',
|
||||
@ -191,6 +180,14 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
systemPrompt = buildSystemPrompt(systemPrompt, mcpTools)
|
||||
}
|
||||
|
||||
let systemMessage: TextBlockParam | undefined = undefined
|
||||
if (systemPrompt) {
|
||||
systemMessage = {
|
||||
type: 'text',
|
||||
text: systemPrompt
|
||||
}
|
||||
}
|
||||
|
||||
const body: MessageCreateParamsNonStreaming = {
|
||||
model: model.id,
|
||||
messages: userMessages,
|
||||
@ -198,9 +195,9 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: this.getTemperature(assistant, model),
|
||||
top_p: this.getTopP(assistant, model),
|
||||
system: systemPrompt,
|
||||
system: systemMessage ? [systemMessage] : undefined,
|
||||
// @ts-ignore thinking
|
||||
thinking: this.getReasoningEffort(assistant, model),
|
||||
thinking: this.getBudgetToken(assistant, model),
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,8 @@ import {
|
||||
ToolListUnion
|
||||
} from '@google/genai'
|
||||
import {
|
||||
isGemini25ReasoningModel,
|
||||
findTokenLimit,
|
||||
isGeminiReasoningModel,
|
||||
isGemmaModel,
|
||||
isGenerateImageModel,
|
||||
isVisionModel,
|
||||
@ -31,6 +32,7 @@ import {
|
||||
} from '@renderer/services/MessagesService'
|
||||
import {
|
||||
Assistant,
|
||||
EFFORT_RATIO,
|
||||
FileType,
|
||||
FileTypes,
|
||||
MCPToolResponse,
|
||||
@ -54,8 +56,6 @@ import OpenAI from 'openai'
|
||||
import { CompletionsParams } from '.'
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
type ReasoningEffort = 'low' | 'medium' | 'high'
|
||||
|
||||
export default class GeminiProvider extends BaseProvider {
|
||||
private sdk: GoogleGenAI
|
||||
|
||||
@ -216,32 +216,36 @@ export default class GeminiProvider extends BaseProvider {
|
||||
* @param model - The model
|
||||
* @returns The reasoning effort
|
||||
*/
|
||||
private getReasoningEffort(assistant: Assistant, model: Model) {
|
||||
if (isGemini25ReasoningModel(model)) {
|
||||
const effortRatios: Record<ReasoningEffort, number> = {
|
||||
high: 1,
|
||||
medium: 0.5,
|
||||
low: 0.2
|
||||
}
|
||||
const effort = assistant?.settings?.reasoning_effort as ReasoningEffort
|
||||
const effortRatio = effortRatios[effort]
|
||||
const maxBudgetToken = 24576 // https://ai.google.dev/gemini-api/docs/thinking
|
||||
const budgetTokens = Math.max(1024, Math.trunc(maxBudgetToken * effortRatio))
|
||||
if (!effortRatio) {
|
||||
private getBudgetToken(assistant: Assistant, model: Model) {
|
||||
if (isGeminiReasoningModel(model)) {
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
// 如果thinking_budget是undefined,不思考
|
||||
if (reasoningEffort === undefined) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0
|
||||
includeThoughts: false
|
||||
} as ThinkingConfig
|
||||
}
|
||||
}
|
||||
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
|
||||
if (effortRatio > 1) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { max } = findTokenLimit(model.id) || { max: 0 }
|
||||
|
||||
// 如果thinking_budget是明确设置的值(包括0),使用该值
|
||||
return {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: budgetTokens,
|
||||
thinkingBudget: Math.floor(max * effortRatio),
|
||||
includeThoughts: true
|
||||
} as ThinkingConfig
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -313,7 +317,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
topP: assistant?.settings?.topP,
|
||||
maxOutputTokens: maxTokens,
|
||||
tools: tools,
|
||||
...this.getReasoningEffort(assistant, model),
|
||||
...this.getBudgetToken(assistant, model),
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import {
|
||||
findTokenLimit,
|
||||
getOpenAIWebSearchParams,
|
||||
isGrokReasoningModel,
|
||||
isHunyuanSearchModel,
|
||||
isOpenAIoSeries,
|
||||
isOpenAIWebSearch,
|
||||
isReasoningModel,
|
||||
isSupportedModel,
|
||||
isSupportedReasoningEffortGrokModel,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedReasoningEffortOpenAIModel,
|
||||
isSupportedThinkingTokenClaudeModel,
|
||||
isSupportedThinkingTokenModel,
|
||||
isSupportedThinkingTokenQwenModel,
|
||||
isVisionModel,
|
||||
isZhipuModel,
|
||||
OPENAI_NO_SUPPORT_DEV_ROLE_MODELS
|
||||
isZhipuModel
|
||||
} from '@renderer/config/models'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
@ -25,6 +28,7 @@ import { processReqMessages } from '@renderer/services/ModelMessageService'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
Assistant,
|
||||
EFFORT_RATIO,
|
||||
FileTypes,
|
||||
GenerateImageParams,
|
||||
MCPToolResponse,
|
||||
@ -59,8 +63,6 @@ import { FileLike } from 'openai/uploads'
|
||||
import { CompletionsParams } from '.'
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
type ReasoningEffort = 'low' | 'medium' | 'high'
|
||||
|
||||
export default class OpenAIProvider extends BaseProvider {
|
||||
private sdk: OpenAI
|
||||
|
||||
@ -262,8 +264,26 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (isReasoningModel(model)) {
|
||||
if (model.provider === 'openrouter') {
|
||||
if (!isReasoningModel(model)) {
|
||||
return {}
|
||||
}
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
if (!reasoningEffort) {
|
||||
if (isSupportedThinkingTokenQwenModel(model)) {
|
||||
return { enable_thinking: false }
|
||||
}
|
||||
|
||||
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||
return { thinking: { type: 'disabled' } }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
const budgetTokens = Math.floor((findTokenLimit(model.id)?.max || 0) * effortRatio)
|
||||
// OpenRouter models
|
||||
if (model.provider === 'openrouter') {
|
||||
if (isSupportedReasoningEffortModel(model)) {
|
||||
return {
|
||||
reasoning: {
|
||||
effort: assistant?.settings?.reasoning_effort
|
||||
@ -271,46 +291,48 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (isGrokReasoningModel(model)) {
|
||||
if (isSupportedThinkingTokenModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpenAIoSeries(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
}
|
||||
|
||||
if (model.id.includes('claude-3.7-sonnet') || model.id.includes('claude-3-7-sonnet')) {
|
||||
const effortRatios: Record<ReasoningEffort, number> = {
|
||||
high: 0.8,
|
||||
medium: 0.5,
|
||||
low: 0.2
|
||||
}
|
||||
|
||||
const effort = assistant?.settings?.reasoning_effort as ReasoningEffort
|
||||
const effortRatio = effortRatios[effort]
|
||||
|
||||
if (!effortRatio) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const maxTokens = assistant?.settings?.maxTokens || DEFAULT_MAX_TOKENS
|
||||
const budgetTokens = Math.trunc(Math.max(Math.min(maxTokens * effortRatio, 32000), 1024))
|
||||
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: budgetTokens
|
||||
reasoning: {
|
||||
max_tokens: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
// Qwen models
|
||||
if (isSupportedThinkingTokenQwenModel(model)) {
|
||||
return {
|
||||
enable_thinking: true,
|
||||
thinking_budget: budgetTokens
|
||||
}
|
||||
}
|
||||
|
||||
// Grok models
|
||||
if (isSupportedReasoningEffortGrokModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI models
|
||||
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
}
|
||||
|
||||
// Claude models
|
||||
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: no special thinking settings
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -343,7 +365,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
const isEnabledWebSearch = assistant.enableWebSearch || !!assistant.webSearchProviderId
|
||||
messages = addImageFileToContents(messages)
|
||||
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
||||
if (isOpenAIoSeries(model) && !OPENAI_NO_SUPPORT_DEV_ROLE_MODELS.includes(model.id)) {
|
||||
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||
systemMessage = {
|
||||
role: 'developer',
|
||||
content: `Formatting re-enabled${systemMessage ? '\n' + systemMessage.content : ''}`
|
||||
|
||||
@ -36,6 +36,16 @@ export type AssistantSettingCustomParameters = {
|
||||
type: 'string' | 'number' | 'boolean' | 'json'
|
||||
}
|
||||
|
||||
export type ReasoningEffortOptions = 'low' | 'medium' | 'high' | 'auto'
|
||||
export type EffortRatio = Record<ReasoningEffortOptions, number>
|
||||
|
||||
export const EFFORT_RATIO: EffortRatio = {
|
||||
low: 0.2,
|
||||
medium: 0.5,
|
||||
high: 1,
|
||||
auto: 2
|
||||
}
|
||||
|
||||
export type AssistantSettings = {
|
||||
contextCount: number
|
||||
temperature: number
|
||||
@ -46,7 +56,7 @@ export type AssistantSettings = {
|
||||
hideMessages: boolean
|
||||
defaultModel?: Model
|
||||
customParameters?: AssistantSettingCustomParameters[]
|
||||
reasoning_effort?: 'low' | 'medium' | 'high'
|
||||
reasoning_effort?: ReasoningEffortOptions
|
||||
}
|
||||
|
||||
export type Agent = Omit<Assistant, 'model'> & {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user