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.
This commit is contained in:
suyao 2025-04-30 15:26:51 +08:00
parent 69e9b9855e
commit 8747974359
No known key found for this signature in database
13 changed files with 330 additions and 177 deletions

View File

@ -1,16 +1,17 @@
import { isSupportedReasoningEffortGrokModel } from '@renderer/config/models'
import { Assistant, Model } from '@renderer/types'
import { Select } from 'antd'
import { useMemo, useState } from 'react'
import { List } from 'antd'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { ReasoningEffortOptions } from './index'
interface ThinkingSelectProps {
model: Model
assistant: Assistant
value?: ReasoningEffortOptions | null
onChange?: (value: ReasoningEffortOptions) => void
value: ReasoningEffortOptions
onChange: (value: ReasoningEffortOptions) => void
}
interface OptionType {
@ -18,9 +19,8 @@ interface OptionType {
value: ReasoningEffortOptions
}
export default function ThinkingSelect({ model, assistant, value, onChange }: ThinkingSelectProps) {
export default function ThinkingSelect({ model, value, onChange }: ThinkingSelectProps) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const baseOptions = useMemo(
() =>
@ -41,24 +41,41 @@ export default function ThinkingSelect({ model, assistant, value, onChange }: Th
[model, baseOptions]
)
const currentValue = value ?? assistant.settings?.reasoning_effort ?? null
const handleChange = (newValue: ReasoningEffortOptions) => {
onChange?.(newValue)
setOpen(false)
}
return (
<>
<Select
placement="topRight"
options={options}
value={currentValue}
onChange={handleChange}
style={{ minWidth: 120 }}
open={open}
onDropdownVisibleChange={setOpen}
/>
</>
<List
dataSource={options}
renderItem={(option) => (
<StyledListItem $isSelected={value === option.value} onClick={() => onChange(option.value)}>
<ReasoningEffortLabel>{option.label}</ReasoningEffortLabel>
</StyledListItem>
)}
/>
)
}
const ReasoningEffortLabel = styled.div`
font-size: 16px;
font-family: Ubuntu;
`
const StyledListItem = styled(List.Item)<{ $isSelected: boolean }>`
cursor: pointer;
padding: 8px 16px;
margin: 4px 0;
font-family: Ubuntu;
border-radius: var(--list-item-border-radius);
font-size: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.3s;
background-color: ${(props) => (props.$isSelected ? 'var(--color-background-soft)' : 'transparent')};
.ant-list-item {
border: none !important;
}
&:hover {
background-color: var(--color-background-soft);
}
`

View File

@ -1,7 +1,9 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { Model } from '@renderer/types'
import { Col, InputNumber, Radio, Row, Slider, Space, Tooltip } from 'antd'
import { Button, InputNumber, Slider, Tooltip } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { isSupportedThinkingTokenGeminiModel } from '../../config/models'
@ -14,11 +16,9 @@ interface ThinkingSliderProps {
}
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<number>(value === null ? 0 : value)
// 当外部value变化时更新内部状态
const { t } = useTranslation()
useEffect(() => {
if (value === null) {
setMode('default')
@ -28,18 +28,15 @@ export default function ThinkingSlider({ model, value, min, max, onChange }: Thi
}
}, [value])
// 处理模式切换
const handleModeChange = (e: any) => {
const newMode = e.target.value
const handleModeChange = (newMode: 'default' | 'custom') => {
setMode(newMode)
if (newMode === 'default') {
onChange(null) // 传递null表示使用默认行为
onChange(null)
} else {
onChange(customValue) // 传递当前自定义值
onChange(customValue)
}
}
// 处理自定义值变化
const handleCustomValueChange = (newValue: number | null) => {
if (newValue !== null) {
setCustomValue(newValue)
@ -48,45 +45,128 @@ export default function ThinkingSlider({ model, value, min, max, onChange }: Thi
}
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Container>
{isSupportedThinkingTokenGeminiModel(model) && (
<Radio.Group value={mode} onChange={handleModeChange}>
<Radio.Button value="default">Default (Model's default behavior)</Radio.Button>
<Radio.Button value="custom">Custom</Radio.Button>
</Radio.Group>
<ButtonGroup>
<Tooltip title={t('chat.input.thinking.mode.default.tip')}>
<ModeButton type={mode === 'default' ? 'primary' : 'text'} onClick={() => handleModeChange('default')}>
{t('chat.input.thinking.mode.default')}
</ModeButton>
</Tooltip>
<Tooltip title={t('chat.input.thinking.mode.custom.tip')}>
<ModeButton type={mode === 'custom' ? 'primary' : 'text'} onClick={() => handleModeChange('custom')}>
{t('chat.input.thinking.mode.custom')}
</ModeButton>
</Tooltip>
</ButtonGroup>
)}
{mode === 'custom' && (
<Row align="middle">
<Col span={12}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Slider
min={min}
max={max}
onChange={handleCustomValueChange}
value={customValue}
marks={{
0: { label: '0' },
[max]: { label: `${max.toLocaleString()}` }
}}
/>
<Tooltip title="Set the number of thinking tokens to use. Set to 0 to explicitly use no thinking tokens.">
<InfoCircleOutlined style={{ marginLeft: 8, color: 'var(--color-text-secondary)' }} />
</Tooltip>
</div>
</Col>
<Col span={4}>
<InputNumber
<CustomControls>
<SliderContainer>
<Slider
min={min}
max={max}
style={{ margin: '0 16px' }}
value={customValue}
onChange={handleCustomValueChange}
addonAfter="tokens"
tooltip={{ formatter: null }}
/>
</Col>
</Row>
<SliderMarks>
<span>0</span>
<span>{max.toLocaleString()}</span>
</SliderMarks>
</SliderContainer>
<InputContainer>
<StyledInputNumber
min={min}
max={max}
value={customValue}
onChange={(value) => handleCustomValueChange(Number(value))}
controls={false}
/>
<Tooltip title={t('chat.input.thinking.mode.tokens.tip')}>
<InfoCircleOutlined style={{ color: 'var(--color-text-2)' }} />
</Tooltip>
</InputContainer>
</CustomControls>
)}
</Space>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
min-width: 320px;
padding: 4px;
`
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 4px;
`
const ModeButton = styled(Button)`
min-width: 90px;
height: 28px;
border-radius: 14px;
padding: 0 16px;
font-size: 13px;
&:hover {
background-color: var(--color-background-soft);
}
&.ant-btn-primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background-color: var(--color-primary);
opacity: 0.9;
}
}
`
const CustomControls = styled.div`
display: flex;
align-items: center;
gap: 12px;
`
const SliderContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 180px;
`
const SliderMarks = styled.div`
display: flex;
justify-content: space-between;
color: var(--color-text-2);
font-size: 12px;
`
const InputContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const StyledInputNumber = styled(InputNumber)`
width: 70px;
.ant-input-number-input {
height: 28px;
text-align: center;
font-size: 13px;
padding: 0 8px;
}
`

View File

@ -14,19 +14,17 @@ import ThinkingSlider from './ThinkingSlider'
const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
// Gemini models
'^gemini-.*$': { min: 0, max: 24576 },
'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 },
'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.*sonnet$': { min: 0, max: 64000 }
'claude-3[.-]7.*sonnet.*$': { min: 0, max: 64000 }
}
export type ReasoningEffortOptions = 'low' | 'medium' | 'high'
@ -67,10 +65,9 @@ export default function ThinkingPanel({ model, assistant }: ThinkingPanelProps)
return currentThinkingBudget > maxTokens
}, [currentThinkingBudget, maxTokens])
// 使用useEffect显示错误消息
useEffect(() => {
if (isBudgetExceedingMax && isSupportedThinkingTokenClaudeModel(model)) {
window.message.error(t('chat.input.thinking_budget_exceeds_max'))
window.message.error(t('chat.input.thinking.budget_exceeds_max'))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isBudgetExceedingMax, model])
@ -109,13 +106,11 @@ export default function ThinkingPanel({ model, assistant }: ThinkingPanelProps)
}
if (isSupportedReasoningEffort) {
const currentReasoningEffort = assistant.settings?.reasoning_effort
return (
<ThinkingSelect
model={model}
assistant={assistant}
value={currentReasoningEffort}
model={model}
value={assistant.settings?.reasoning_effort || 'medium'}
onChange={onReasoningEffortChange}
/>
)

View File

@ -210,6 +210,7 @@ export const FUNCTION_CALLING_MODELS = [
'o1(?:-[\\w-]+)?',
'claude',
'qwen',
'qwen3',
'hunyuan',
'deepseek',
'glm-4(?:-[\\w-]+)?',
@ -2298,7 +2299,7 @@ export function isQwenReasoningModel(model?: Model): boolean {
return true
}
if (model.id.includes('qwq')) {
if (model.id.includes('qwq') || model.id.includes('qvq')) {
return true
}
@ -2312,8 +2313,6 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
return (
model.id.includes('qwen3') ||
model.id.includes('qwq') ||
model.id.includes('qvq') ||
[
'qwen-plus-latest',
'qwen-plus-0428',

View File

@ -56,11 +56,9 @@
"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.high": "Think harder",
"settings.reasoning_effort.low": "Think less",
"settings.reasoning_effort.medium": "Think normally",
"settings.more": "Assistant Settings"
},
"auth": {
@ -185,7 +183,6 @@
"settings.top_p": "Top-P",
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
"suggestions.title": "Suggested Questions",
"thinking": "Thinking",
"topics.auto_rename": "Auto Rename",
"topics.clear.title": "Clear Messages",
"topics.copy.image": "Copy as image",
@ -250,7 +247,15 @@
"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.",
"thinking": "Thinking",
"input.thinking.budget_exceeds_max": "Thinking budget exceeds the maximum token number"
},
"code_block": {
"collapse": "Collapse",

View File

@ -56,11 +56,9 @@
"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.more": "アシスタント設定"
},
"auth": {
@ -250,7 +248,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": "折りたたむ",
@ -1507,7 +1512,7 @@
"title": "プライバシー設定",
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
},
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
"input.show_translate_confirm": "翻訳確認ダイアログを表示"
},
"translate": {
"any.language": "任意の言語",

View File

@ -55,12 +55,9 @@
"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.high": "Стараюсь думать",
"settings.reasoning_effort.low": "Меньше думать",
"settings.reasoning_effort.medium": "Среднее",
"settings.more": "Настройки ассистента"
},
"auth": {
@ -250,7 +247,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": "Свернуть",
@ -1507,7 +1511,7 @@
"title": "Настройки приватности",
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
},
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода"
},
"translate": {
"any.language": "Любой язык",

View File

@ -56,11 +56,9 @@
"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.low": "稍微思考",
"settings.reasoning_effort.medium": "正常思考",
"settings.reasoning_effort.high": "尽力思考",
"settings.more": "助手设置"
},
"auth": {
@ -133,6 +131,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": "上传图片或文档",

View File

@ -56,11 +56,9 @@
"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.more": "助手設定"
},
"auth": {
@ -250,7 +248,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": "折疊",

View File

@ -799,13 +799,17 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const newEnableThinking = !assistant.enableThinking
updateAssistant({ ...assistant, enableThinking: newEnableThinking })
if (newEnableThinking && (isSupportedThinkingToken || isSupportedReasoningEffort)) {
setThinkingPanelVisible(true)
} else {
if (!newEnableThinking) {
setThinkingPanelVisible(false)
}
}
const onTogglePanel = () => {
if (isSupportedThinkingToken || isSupportedReasoningEffort) {
setThinkingPanelVisible((prev) => !prev)
}
}
useEffect(() => {
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
updateAssistant({ ...assistant, enableWebSearch: false })
@ -952,6 +956,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
assistant={assistant}
ToolbarButton={ToolbarButton}
onToggleThinking={onToggleThinking}
onTogglePanel={onTogglePanel}
showPanel={thinkingPanelVisible}
/>
</ThinkingButtonContainer>
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
@ -1139,7 +1145,8 @@ const ToolbarButton = styled(Button)`
&.active {
background-color: var(--color-primary) !important;
.anticon,
.iconfont {
.iconfont,
.chevron-icon {
color: var(--color-white-soft);
}
&:hover {
@ -1155,31 +1162,18 @@ const ThinkingPanelContainer = styled.div`
transform: translateX(-50%);
background-color: var(--color-background);
border: 0.5px solid var(--color-border);
border-radius: 8px 8px 0 0;
border-radius: 15px;
padding: 10px;
margin-bottom: 5px;
z-index: 10;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
min-width: 250px;
/* Add a small arrow pointing down to the ThinkingButton */
&::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid var(--color-border);
}
`
const ThinkingButtonContainer = styled.div`
position: relative;
display: inline-block;
display: inline-flex;
align-items: center;
`
export default Inputbar

View File

@ -1,31 +1,72 @@
import { isReasoningModel } from '@renderer/config/models'
import {
isReasoningModel,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel
} from '@renderer/config/models'
import { Assistant, Model } from '@renderer/types'
import { Tooltip } from 'antd'
import { Brain } from 'lucide-react'
import { Atom, ChevronDown, ChevronUp } from 'lucide-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
model: Model
assistant: Assistant
ToolbarButton: any
onToggleThinking: () => void
onTogglePanel: () => void
showPanel: boolean
}
const ThinkingButton: FC<Props> = ({ model, assistant, ToolbarButton, onToggleThinking }) => {
const ThinkingButton: FC<Props> = ({ model, assistant, ToolbarButton, onToggleThinking, onTogglePanel, showPanel }) => {
const { t } = useTranslation()
if (!isReasoningModel(model)) {
return null
}
const isSupportedThinkingToken = isSupportedThinkingTokenModel(model)
const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model)
return (
<Tooltip placement="top" title={t('chat.input.thinking')} arrow>
<ToolbarButton type="text" disabled={!isReasoningModel(model)} onClick={onToggleThinking}>
<Brain size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
</ToolbarButton>
<ButtonContainer>
<ToolbarButton type="text" disabled={!isReasoningModel(model)} onClick={onToggleThinking}>
<Atom size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
</ToolbarButton>
<ChevronButton onClick={onTogglePanel} disabled={!isSupportedThinkingToken && !isSupportedReasoningEffort}>
{showPanel ? (
<ChevronUp size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
) : (
<ChevronDown size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
)}
</ChevronButton>
</ButtonContainer>
</Tooltip>
)
}
const ButtonContainer = styled.div`
display: flex;
align-items: center;
gap: 0px;
`
const ChevronButton = styled.button`
background: none;
border: none;
padding: 2px;
cursor: pointer;
color: var(--color-icon);
display: flex;
align-items: center;
justify-content: center;
margin-left: -8px;
transition: all 0.3s;
&:hover {
color: var(--color-text-1);
}
`
export default ThinkingButton

View File

@ -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,17 @@ 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 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 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 = assistant?.settings?.thinking_budget || maxTokens
if (budgetTokens > maxTokens) {
return undefined
}
return {
type: 'enabled',
@ -200,7 +183,7 @@ export default class AnthropicProvider extends BaseProvider {
top_p: this.getTopP(assistant, model),
system: systemPrompt,
// @ts-ignore thinking
thinking: this.getReasoningEffort(assistant, model),
thinking: this.getBudgetToken(assistant, model),
...this.getCustomParameters(assistant)
}

View File

@ -1,10 +1,10 @@
import {
getOpenAIWebSearchParams,
isGrokReasoningModel,
isHunyuanSearchModel,
isOpenAIWebSearch,
isReasoningModel,
isSupportedModel,
isSupportedReasoningEffortGrokModel,
isSupportedReasoningEffortModel,
isSupportedReasoningEffortOpenAIModel,
isSupportedThinkingTokenClaudeModel,
@ -264,11 +264,15 @@ export default class OpenAIProvider extends BaseProvider {
if (model.provider === 'openrouter') {
if (isSupportedReasoningEffortModel(model)) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
reasoning: {
effort: assistant?.settings?.reasoning_effort
}
}
} else if (isSupportedThinkingTokenModel(model)) {
return {
max_tokens: assistant?.settings?.thinking_budget
reasoning: {
max_tokens: assistant?.settings?.thinking_budget
}
}
}
}
@ -286,23 +290,39 @@ export default class OpenAIProvider extends BaseProvider {
}
}
if (isGrokReasoningModel(model)) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
if (isSupportedReasoningEffortGrokModel(model)) {
if (enableThinking) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
}
} else {
return {}
}
}
if (isSupportedReasoningEffortOpenAIModel(model)) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
if (enableThinking) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
}
} else {
return {}
}
}
if (isSupportedThinkingTokenClaudeModel(model)) {
return {
thinking: {
type: 'enabled',
budget_tokens: assistant?.settings?.thinking_budget
if (enableThinking) {
return {
thinking: {
type: 'enabled',
budget_tokens: assistant?.settings?.thinking_budget
}
}
} else {
return {
thinking: {
type: 'disabled'
}
}
}
}