diff --git a/src/renderer/src/components/ThinkingPanel/ThinkingSelect.tsx b/src/renderer/src/components/ThinkingPanel/ThinkingSelect.tsx
new file mode 100644
index 0000000000..307dd4d4f4
--- /dev/null
+++ b/src/renderer/src/components/ThinkingPanel/ThinkingSelect.tsx
@@ -0,0 +1,64 @@
+import { isSupportedReasoningEffortGrokModel } from '@renderer/config/models'
+import { Assistant, Model } from '@renderer/types'
+import { Select } from 'antd'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import { ReasoningEffortOptions } from './index'
+
+interface ThinkingSelectProps {
+ model: Model
+ assistant: Assistant
+ value?: ReasoningEffortOptions | null
+ onChange?: (value: ReasoningEffortOptions) => void
+}
+
+interface OptionType {
+ label: string
+ value: ReasoningEffortOptions
+}
+
+export default function ThinkingSelect({ model, assistant, value, onChange }: ThinkingSelectProps) {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+
+ const baseOptions = useMemo(
+ () =>
+ [
+ { label: t('assistants.settings.reasoning_effort.low'), value: 'low' },
+ { label: t('assistants.settings.reasoning_effort.medium'), value: 'medium' },
+ { label: t('assistants.settings.reasoning_effort.high'), value: 'high' }
+ ] as OptionType[],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ )
+
+ const options = useMemo(
+ () =>
+ isSupportedReasoningEffortGrokModel(model)
+ ? baseOptions.filter((option) => option.value === 'low' || option.value === 'high')
+ : baseOptions,
+ [model, baseOptions]
+ )
+
+ const currentValue = value ?? assistant.settings?.reasoning_effort ?? null
+
+ const handleChange = (newValue: ReasoningEffortOptions) => {
+ onChange?.(newValue)
+ setOpen(false)
+ }
+
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/src/renderer/src/components/ThinkingPanel/ThinkingSlider.tsx b/src/renderer/src/components/ThinkingPanel/ThinkingSlider.tsx
new file mode 100644
index 0000000000..d49ee28bc1
--- /dev/null
+++ b/src/renderer/src/components/ThinkingPanel/ThinkingSlider.tsx
@@ -0,0 +1,92 @@
+import { InfoCircleOutlined } from '@ant-design/icons'
+import { Model } from '@renderer/types'
+import { Col, InputNumber, Radio, Row, Slider, Space, Tooltip } from 'antd'
+import { useEffect, useState } from 'react'
+
+import { isSupportedThinkingTokenGeminiModel } from '../../config/models'
+
+interface ThinkingSliderProps {
+ model: Model
+ value: number | null
+ min: number
+ max: number
+ onChange: (value: number | null) => void
+}
+
+export default function ThinkingSlider({ model, value, min, max, onChange }: ThinkingSliderProps) {
+ // 使用null表示"Default"模式,使用数字表示"Custom"模式
+ const [mode, setMode] = useState<'default' | 'custom'>(value === null ? 'default' : 'custom')
+ const [customValue, setCustomValue] = useState(value === null ? 0 : value)
+
+ // 当外部value变化时更新内部状态
+ useEffect(() => {
+ if (value === null) {
+ setMode('default')
+ } else {
+ setMode('custom')
+ setCustomValue(value)
+ }
+ }, [value])
+
+ // 处理模式切换
+ const handleModeChange = (e: any) => {
+ const newMode = e.target.value
+ setMode(newMode)
+ if (newMode === 'default') {
+ onChange(null) // 传递null表示使用默认行为
+ } else {
+ onChange(customValue) // 传递当前自定义值
+ }
+ }
+
+ // 处理自定义值变化
+ const handleCustomValueChange = (newValue: number | null) => {
+ if (newValue !== null) {
+ setCustomValue(newValue)
+ onChange(newValue)
+ }
+ }
+
+ return (
+
+ {isSupportedThinkingTokenGeminiModel(model) && (
+
+ Default (Model's default behavior)
+ Custom
+
+ )}
+
+ {mode === 'custom' && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/src/renderer/src/components/ThinkingPanel/index.tsx b/src/renderer/src/components/ThinkingPanel/index.tsx
new file mode 100644
index 0000000000..4a9f8f6d68
--- /dev/null
+++ b/src/renderer/src/components/ThinkingPanel/index.tsx
@@ -0,0 +1,125 @@
+import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
+import {
+ isSupportedReasoningEffortModel,
+ isSupportedThinkingTokenClaudeModel,
+ isSupportedThinkingTokenModel
+} from '@renderer/config/models'
+import { useAssistant } from '@renderer/hooks/useAssistant'
+import { Assistant, Model } from '@renderer/types'
+import { useCallback, useEffect, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import ThinkingSelect from './ThinkingSelect'
+import ThinkingSlider from './ThinkingSlider'
+
+const THINKING_TOKEN_MAP: Record = {
+ // Gemini models
+ '^gemini-.*$': { min: 0, max: 24576 },
+
+ // Qwen models
+ '^qwen-plus-.*$': { min: 0, max: 38912 },
+ '^qwen-turbo-.*$': { min: 0, max: 38912 },
+ '^qwq-.*$': { min: 0, max: 32768 },
+ '^qvq-.*$': { min: 0, max: 16384 },
+ '^qwen3-0\\.6b$': { min: 0, max: 30720 },
+ '^qwen3-1\\.7b$': { min: 0, max: 30720 },
+ '^qwen3-.*$': { min: 0, max: 38912 },
+
+ // Claude models
+ '^claude-3.*sonnet$': { min: 0, max: 64000 }
+}
+
+export type ReasoningEffortOptions = 'low' | 'medium' | 'high'
+
+// Helper function to find matching token limit
+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
+}
+
+interface ThinkingPanelProps {
+ model: Model
+ assistant: Assistant
+}
+
+export default function ThinkingPanel({ model, assistant }: ThinkingPanelProps) {
+ const { updateAssistantSettings } = useAssistant(assistant.id)
+ const isSupportedThinkingToken = isSupportedThinkingTokenModel(model)
+ const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model)
+ const thinkingTokenRange = findTokenLimit(model.id)
+ const { t } = useTranslation()
+
+ // 获取当前的thinking_budget值
+ // 如果thinking_budget未设置,则使用null表示默认行为
+ const currentThinkingBudget =
+ assistant.settings?.thinking_budget !== undefined ? assistant.settings.thinking_budget : null
+
+ // 获取maxTokens值
+ const maxTokens = assistant.settings?.maxTokens || DEFAULT_MAX_TOKENS
+
+ // 检查budgetTokens是否大于maxTokens
+ const isBudgetExceedingMax = useMemo(() => {
+ if (currentThinkingBudget === null) return false
+ return currentThinkingBudget > maxTokens
+ }, [currentThinkingBudget, maxTokens])
+
+ // 使用useEffect显示错误消息
+ useEffect(() => {
+ if (isBudgetExceedingMax && isSupportedThinkingTokenClaudeModel(model)) {
+ window.message.error(t('chat.input.thinking_budget_exceeds_max'))
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isBudgetExceedingMax, model])
+
+ const onTokenChange = useCallback(
+ (value: number | null) => {
+ // 如果值为null,则删除thinking_budget设置,使用默认行为
+ if (value === null) {
+ updateAssistantSettings({ thinking_budget: undefined })
+ } else {
+ updateAssistantSettings({ thinking_budget: value })
+ }
+ },
+ [updateAssistantSettings]
+ )
+
+ const onReasoningEffortChange = useCallback(
+ (value: ReasoningEffortOptions) => {
+ updateAssistantSettings({ reasoning_effort: value })
+ },
+ [updateAssistantSettings]
+ )
+
+ if (isSupportedThinkingToken) {
+ return (
+ <>
+
+ >
+ )
+ }
+
+ if (isSupportedReasoningEffort) {
+ const currentReasoningEffort = assistant.settings?.reasoning_effort
+
+ return (
+
+ )
+ }
+
+ return null
+}
diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts
index c1ed7e84ed..a45f263353 100644
--- a/src/renderer/src/config/models.ts
+++ b/src/renderer/src/config/models.ts
@@ -2218,30 +2218,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 +2273,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 +2287,53 @@ 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')) {
+ return true
+ }
+
+ return false
+}
+
+export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
+ if (!model) {
+ return false
+ }
+
+ return (
+ model.id.includes('qwen3') ||
+ model.id.includes('qwq') ||
+ model.id.includes('qvq') ||
+ [
+ '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 +2343,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
}
diff --git a/src/renderer/src/locales/zh/translation.json b/src/renderer/src/locales/zh/translation.json
new file mode 100644
index 0000000000..0519ecba6e
--- /dev/null
+++ b/src/renderer/src/locales/zh/translation.json
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
index 6cb8559af8..3a2554caf6 100644
--- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
+++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
@@ -1,7 +1,13 @@
import { HolderOutlined } from '@ant-design/icons'
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
+import ThinkingPanel from '@renderer/components/ThinkingPanel'
import TranslateButton from '@renderer/components/TranslateButton'
-import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
+import {
+ isGenerateImageModel,
+ isSupportedReasoningEffortModel,
+ isVisionModel,
+ isWebSearchModel
+} from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
@@ -53,6 +59,7 @@ import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useS
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
+import { isSupportedThinkingTokenModel } from '../../../config/models'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview'
@@ -65,6 +72,7 @@ import MentionModelsInput from './MentionModelsInput'
import NewContextButton from './NewContextButton'
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
import SendMessageButton from './SendMessageButton'
+import ThinkingButton from './ThinkingButton'
import TokenCount from './TokenCount'
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
@@ -110,6 +118,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([])
const [mentionModels, setMentionModels] = useState([])
const [isDragging, setIsDragging] = useState(false)
+ const [thinkingPanelVisible, setThinkingPanelVisible] = useState(false)
const [textareaHeight, setTextareaHeight] = useState()
const startDragY = useRef(0)
const startHeight = useRef(0)
@@ -761,6 +770,9 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
+ const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model)
+ const isSupportedThinkingToken = isSupportedThinkingTokenModel(model)
+
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
updateAssistant({ ...assistant, knowledge_bases: bases })
setSelectedKnowledgeBases(bases ?? [])
@@ -783,6 +795,17 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
}
+ const onToggleThinking = () => {
+ const newEnableThinking = !assistant.enableThinking
+ updateAssistant({ ...assistant, enableThinking: newEnableThinking })
+
+ if (newEnableThinking && (isSupportedThinkingToken || isSupportedReasoningEffort)) {
+ setThinkingPanelVisible(true)
+ } else {
+ setThinkingPanelVisible(false)
+ }
+ }
+
useEffect(() => {
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
updateAssistant({ ...assistant, enableWebSearch: false })
@@ -918,6 +941,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
setFiles={setFiles}
ToolbarButton={ToolbarButton}
/>
+
+ {thinkingPanelVisible && (
+
+
+
+ )}
+
+
{showKnowledgeIcon && (
void
+}
+
+const ThinkingButton: FC = ({ model, assistant, ToolbarButton, onToggleThinking }) => {
+ const { t } = useTranslation()
+
+ if (!isReasoningModel(model)) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default ThinkingButton
diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
index 0bef734c3c..9a21b1c808 100644
--- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
@@ -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) => {
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) => {
}
}
- 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) => {
maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true,
hideMessages: false,
- reasoning_effort: undefined,
customParameters: []
}
})
@@ -160,25 +148,8 @@ const SettingsTab: FC = (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) => {
)}
- {isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && (
- <>
-
-
-
-
-
-
-
-
-
-
- {
- 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
- />
-
-
-
- >
- )}
{t('settings.messages.title')}
@@ -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;
diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx
index d7604ed79f..24b6025a12 100644
--- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx
+++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx
@@ -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 = ({ 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 = ({ 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 = ({ assistant, updateAssistant, updateA
setMaxTokens(0)
setStreamOutput(true)
setTopP(1)
- setReasoningEffort(undefined)
setCustomParameters([])
updateAssistantSettings({
temperature: DEFAULT_TEMPERATURE,
@@ -162,7 +156,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA
maxTokens: 0,
streamOutput: true,
topP: 1,
- reasoning_effort: undefined,
customParameters: []
})
}
@@ -383,27 +376,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA
/>
-
-
- {
- setReasoningEffort(e.target.value)
- onReasoningEffortChange(e.target.value)
- }}>
- {t('assistants.settings.reasoning_effort.low')}
- {t('assistants.settings.reasoning_effort.medium')}
- {t('assistants.settings.reasoning_effort.high')}
- {t('assistants.settings.reasoning_effort.off')}
-
-
-
} onClick={onAddCustomParameter}>
diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts
index 09beed9b9b..18556e5d6c 100644
--- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts
+++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts
@@ -14,7 +14,7 @@ import {
ToolListUnion
} from '@google/genai'
import {
- isGemini25ReasoningModel,
+ isGeminiReasoningModel,
isGemmaModel,
isGenerateImageModel,
isVisionModel,
@@ -54,8 +54,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
@@ -213,32 +211,25 @@ 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 = {
- 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) {
- return {
- thinkingConfig: {
- thinkingBudget: 0
- } as ThinkingConfig
- }
+ private getBudgetToken(assistant: Assistant, model: Model) {
+ if (isGeminiReasoningModel(model)) {
+ // 检查thinking_budget是否明确设置
+ const thinkingBudget = assistant?.settings?.thinking_budget
+
+ // 如果thinking_budget是undefined,使用模型的默认行为
+ if (thinkingBudget === undefined) {
+ return {} // 返回空对象以使用模型默认值
}
+ // 如果thinking_budget是明确设置的值(包括0),使用该值
return {
thinkingConfig: {
- thinkingBudget: budgetTokens,
+ thinkingBudget: thinkingBudget,
includeThoughts: true
} as ThinkingConfig
}
}
+
return {}
}
@@ -310,7 +301,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)
}
diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts
index fb918f5ce7..87711d8dcf 100644
--- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts
+++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts
@@ -1,15 +1,17 @@
-import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import {
getOpenAIWebSearchParams,
isGrokReasoningModel,
isHunyuanSearchModel,
- isOpenAIoSeries,
isOpenAIWebSearch,
isReasoningModel,
isSupportedModel,
+ 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'
@@ -57,8 +59,6 @@ import {
import { CompletionsParams } from '.'
import BaseProvider from './BaseProvider'
-type ReasoningEffort = 'low' | 'medium' | 'high'
-
export default class OpenAIProvider extends BaseProvider {
private sdk: OpenAI
@@ -262,9 +262,26 @@ export default class OpenAIProvider extends BaseProvider {
if (isReasoningModel(model)) {
if (model.provider === 'openrouter') {
- return {
- reasoning: {
- effort: assistant?.settings?.reasoning_effort
+ if (isSupportedReasoningEffortModel(model)) {
+ return {
+ reasoning_effort: assistant?.settings?.reasoning_effort
+ }
+ } else if (isSupportedThinkingTokenModel(model)) {
+ return {
+ max_tokens: assistant?.settings?.thinking_budget
+ }
+ }
+ }
+ const enableThinking = assistant?.enableThinking
+ if (isSupportedThinkingTokenQwenModel(model)) {
+ if (enableThinking) {
+ return {
+ enable_thinking: true,
+ thinking_budget: assistant?.settings?.thinking_budget
+ }
+ } else {
+ return {
+ enable_thinking: false
}
}
}
@@ -275,33 +292,17 @@ export default class OpenAIProvider extends BaseProvider {
}
}
- if (isOpenAIoSeries(model)) {
+ if (isSupportedReasoningEffortOpenAIModel(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 = {
- 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))
-
+ if (isSupportedThinkingTokenClaudeModel(model)) {
return {
thinking: {
type: 'enabled',
- budget_tokens: budgetTokens
+ budget_tokens: assistant?.settings?.thinking_budget
}
}
}
@@ -341,7 +342,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 : ''}`
diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts
index 6180ada35a..5f90e2bb02 100644
--- a/src/renderer/src/types/index.ts
+++ b/src/renderer/src/types/index.ts
@@ -22,6 +22,7 @@ export type Assistant = {
enableWebSearch?: boolean
webSearchProviderId?: WebSearchProvider['id']
enableGenerateImage?: boolean
+ enableThinking?: boolean
mcpServers?: MCPServer[]
}
@@ -47,6 +48,7 @@ export type AssistantSettings = {
defaultModel?: Model
customParameters?: AssistantSettingCustomParameters[]
reasoning_effort?: 'low' | 'medium' | 'high'
+ thinking_budget?: number
}
export type Agent = Omit & {