mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
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.
This commit is contained in:
parent
1c5526c020
commit
69e9b9855e
64
src/renderer/src/components/ThinkingPanel/ThinkingSelect.tsx
Normal file
64
src/renderer/src/components/ThinkingPanel/ThinkingSelect.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
placement="topRight"
|
||||||
|
options={options}
|
||||||
|
value={currentValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ minWidth: 120 }}
|
||||||
|
open={open}
|
||||||
|
onDropdownVisibleChange={setOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
92
src/renderer/src/components/ThinkingPanel/ThinkingSlider.tsx
Normal file
92
src/renderer/src/components/ThinkingPanel/ThinkingSlider.tsx
Normal file
@ -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<number>(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 (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
style={{ margin: '0 16px' }}
|
||||||
|
value={customValue}
|
||||||
|
onChange={handleCustomValueChange}
|
||||||
|
addonAfter="tokens"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
125
src/renderer/src/components/ThinkingPanel/index.tsx
Normal file
125
src/renderer/src/components/ThinkingPanel/index.tsx
Normal file
@ -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<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 },
|
||||||
|
'^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 (
|
||||||
|
<>
|
||||||
|
<ThinkingSlider
|
||||||
|
model={model}
|
||||||
|
value={currentThinkingBudget}
|
||||||
|
min={thinkingTokenRange?.min ?? 0}
|
||||||
|
max={thinkingTokenRange?.max ?? 0}
|
||||||
|
onChange={onTokenChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSupportedReasoningEffort) {
|
||||||
|
const currentReasoningEffort = assistant.settings?.reasoning_effort
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThinkingSelect
|
||||||
|
model={model}
|
||||||
|
assistant={assistant}
|
||||||
|
value={currentReasoningEffort}
|
||||||
|
onChange={onReasoningEffortChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@ -2218,30 +2218,40 @@ export function isVisionModel(model: Model): boolean {
|
|||||||
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
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')
|
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 {
|
export function isOpenAIWebSearch(model: Model): boolean {
|
||||||
return model.id.includes('gpt-4o-search-preview') || model.id.includes('gpt-4o-mini-search-preview')
|
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 {
|
export function isSupportedReasoningEffortModel(model?: Model): boolean {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return isSupportedReasoningEffortOpenAIModel(model) || isSupportedReasoningEffortGrokModel(model)
|
||||||
model.id.includes('claude-3-7-sonnet') ||
|
|
||||||
model.id.includes('claude-3.7-sonnet') ||
|
|
||||||
isOpenAIoSeries(model) ||
|
|
||||||
isGrokReasoningModel(model) ||
|
|
||||||
isGemini25ReasoningModel(model)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGrokModel(model?: Model): boolean {
|
export function isGrokModel(model?: Model): boolean {
|
||||||
@ -2263,7 +2273,9 @@ export function isGrokReasoningModel(model?: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGemini25ReasoningModel(model?: Model): boolean {
|
export const isSupportedReasoningEffortGrokModel = isGrokReasoningModel
|
||||||
|
|
||||||
|
export function isGeminiReasoningModel(model?: Model): boolean {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -2275,6 +2287,53 @@ export function isGemini25ReasoningModel(model?: Model): boolean {
|
|||||||
return false
|
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 {
|
export function isReasoningModel(model?: Model): boolean {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
@ -2284,15 +2343,14 @@ export function isReasoningModel(model?: Model): boolean {
|
|||||||
return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false
|
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)) {
|
if (
|
||||||
return true
|
isClaudeReasoningModel(model) ||
|
||||||
}
|
isOpenAIReasoningModel(model) ||
|
||||||
|
isGeminiReasoningModel(model) ||
|
||||||
if (isGemini25ReasoningModel(model)) {
|
isQwenReasoningModel(model) ||
|
||||||
return true
|
isGrokReasoningModel(model) ||
|
||||||
}
|
model.id.includes('glm-z1')
|
||||||
|
) {
|
||||||
if (model.id.includes('glm-z1')) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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,13 @@
|
|||||||
import { HolderOutlined } from '@ant-design/icons'
|
import { HolderOutlined } from '@ant-design/icons'
|
||||||
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
|
import ThinkingPanel from '@renderer/components/ThinkingPanel'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
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 db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { isSupportedThinkingTokenModel } from '../../../config/models'
|
||||||
import NarrowLayout from '../Messages/NarrowLayout'
|
import NarrowLayout from '../Messages/NarrowLayout'
|
||||||
import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton'
|
import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton'
|
||||||
import AttachmentPreview from './AttachmentPreview'
|
import AttachmentPreview from './AttachmentPreview'
|
||||||
@ -65,6 +72,7 @@ import MentionModelsInput from './MentionModelsInput'
|
|||||||
import NewContextButton from './NewContextButton'
|
import NewContextButton from './NewContextButton'
|
||||||
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
import ThinkingButton from './ThinkingButton'
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
|
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
|
||||||
|
|
||||||
@ -110,6 +118,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const [thinkingPanelVisible, setThinkingPanelVisible] = useState(false)
|
||||||
const [textareaHeight, setTextareaHeight] = useState<number>()
|
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||||
const startDragY = useRef<number>(0)
|
const startDragY = useRef<number>(0)
|
||||||
const startHeight = useRef<number>(0)
|
const startHeight = useRef<number>(0)
|
||||||
@ -761,6 +770,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
|
|
||||||
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
||||||
|
|
||||||
|
const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model)
|
||||||
|
const isSupportedThinkingToken = isSupportedThinkingTokenModel(model)
|
||||||
|
|
||||||
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
|
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
|
||||||
updateAssistant({ ...assistant, knowledge_bases: bases })
|
updateAssistant({ ...assistant, knowledge_bases: bases })
|
||||||
setSelectedKnowledgeBases(bases ?? [])
|
setSelectedKnowledgeBases(bases ?? [])
|
||||||
@ -783,6 +795,17 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
|
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
|
||||||
updateAssistant({ ...assistant, enableWebSearch: false })
|
updateAssistant({ ...assistant, enableWebSearch: false })
|
||||||
@ -918,6 +941,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
/>
|
/>
|
||||||
|
<ThinkingButtonContainer>
|
||||||
|
{thinkingPanelVisible && (
|
||||||
|
<ThinkingPanelContainer>
|
||||||
|
<ThinkingPanel model={model} assistant={assistant} />
|
||||||
|
</ThinkingPanelContainer>
|
||||||
|
)}
|
||||||
|
<ThinkingButton
|
||||||
|
model={model}
|
||||||
|
assistant={assistant}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
onToggleThinking={onToggleThinking}
|
||||||
|
/>
|
||||||
|
</ThinkingButtonContainer>
|
||||||
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||||
{showKnowledgeIcon && (
|
{showKnowledgeIcon && (
|
||||||
<KnowledgeBaseButton
|
<KnowledgeBaseButton
|
||||||
@ -1112,4 +1148,38 @@ const ToolbarButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const ThinkingPanelContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
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;
|
||||||
|
`
|
||||||
|
|
||||||
export default Inputbar
|
export default Inputbar
|
||||||
|
|||||||
31
src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx
Normal file
31
src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { isReasoningModel } from '@renderer/config/models'
|
||||||
|
import { Assistant, Model } from '@renderer/types'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { Brain } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
model: Model
|
||||||
|
assistant: Assistant
|
||||||
|
ToolbarButton: any
|
||||||
|
onToggleThinking: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThinkingButton: FC<Props> = ({ model, assistant, ToolbarButton, onToggleThinking }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!isReasoningModel(model)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThinkingButton
|
||||||
@ -8,13 +8,11 @@ import {
|
|||||||
isMac,
|
isMac,
|
||||||
isWindows
|
isWindows
|
||||||
} from '@renderer/config/constant'
|
} from '@renderer/config/constant'
|
||||||
import { isGrokReasoningModel, isSupportedReasoningEffortModel } from '@renderer/config/models'
|
|
||||||
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
||||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
SendMessageShortcut,
|
SendMessageShortcut,
|
||||||
@ -52,9 +50,9 @@ import {
|
|||||||
TranslateLanguageVarious
|
TranslateLanguageVarious
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
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 { 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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -72,7 +70,6 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||||
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
||||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||||
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort)
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
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 = () => {
|
const onReset = () => {
|
||||||
setTemperature(DEFAULT_TEMPERATURE)
|
setTemperature(DEFAULT_TEMPERATURE)
|
||||||
setContextCount(DEFAULT_CONTEXTCOUNT)
|
setContextCount(DEFAULT_CONTEXTCOUNT)
|
||||||
setReasoningEffort(undefined)
|
|
||||||
updateAssistant({
|
updateAssistant({
|
||||||
...assistant,
|
...assistant,
|
||||||
settings: {
|
settings: {
|
||||||
@ -148,7 +137,6 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
maxTokens: DEFAULT_MAX_TOKENS,
|
maxTokens: DEFAULT_MAX_TOKENS,
|
||||||
streamOutput: true,
|
streamOutput: true,
|
||||||
hideMessages: false,
|
hideMessages: false,
|
||||||
reasoning_effort: undefined,
|
|
||||||
customParameters: []
|
customParameters: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -160,25 +148,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
|
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
|
||||||
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
||||||
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
||||||
setReasoningEffort(assistant?.settings?.reasoning_effort)
|
|
||||||
}, [assistant])
|
}, [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) => {
|
const formatSliderTooltip = (value?: number) => {
|
||||||
if (value === undefined) return ''
|
if (value === undefined) return ''
|
||||||
return value === 20 ? '∞' : value.toString()
|
return value === 20 ? '∞' : value.toString()
|
||||||
@ -294,46 +265,6 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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>
|
||||||
<SettingGroup>
|
<SettingGroup>
|
||||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
||||||
@ -706,27 +637,6 @@ export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
|||||||
margin-bottom: 10px;
|
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)`
|
const StyledSelect = styled(Select)`
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border-radius: 15px !important;
|
border-radius: 15px !important;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/cons
|
|||||||
import { SettingRow } from '@renderer/pages/settings'
|
import { SettingRow } from '@renderer/pages/settings'
|
||||||
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
|
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
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 { isNull } from 'lodash'
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
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 [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
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) => {
|
const onContextCountChange = (value) => {
|
||||||
if (!isNaN(value as number)) {
|
if (!isNaN(value as number)) {
|
||||||
updateAssistantSettings({ contextCount: value })
|
updateAssistantSettings({ contextCount: value })
|
||||||
@ -153,7 +148,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
setMaxTokens(0)
|
setMaxTokens(0)
|
||||||
setStreamOutput(true)
|
setStreamOutput(true)
|
||||||
setTopP(1)
|
setTopP(1)
|
||||||
setReasoningEffort(undefined)
|
|
||||||
setCustomParameters([])
|
setCustomParameters([])
|
||||||
updateAssistantSettings({
|
updateAssistantSettings({
|
||||||
temperature: DEFAULT_TEMPERATURE,
|
temperature: DEFAULT_TEMPERATURE,
|
||||||
@ -162,7 +156,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
maxTokens: 0,
|
maxTokens: 0,
|
||||||
streamOutput: true,
|
streamOutput: true,
|
||||||
topP: 1,
|
topP: 1,
|
||||||
reasoning_effort: undefined,
|
|
||||||
customParameters: []
|
customParameters: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -383,27 +376,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<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 }}>
|
<SettingRow style={{ minHeight: 30 }}>
|
||||||
<Label>{t('models.custom_parameters')}</Label>
|
<Label>{t('models.custom_parameters')}</Label>
|
||||||
<Button icon={<PlusOutlined />} onClick={onAddCustomParameter}>
|
<Button icon={<PlusOutlined />} onClick={onAddCustomParameter}>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
ToolListUnion
|
ToolListUnion
|
||||||
} from '@google/genai'
|
} from '@google/genai'
|
||||||
import {
|
import {
|
||||||
isGemini25ReasoningModel,
|
isGeminiReasoningModel,
|
||||||
isGemmaModel,
|
isGemmaModel,
|
||||||
isGenerateImageModel,
|
isGenerateImageModel,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
@ -54,8 +54,6 @@ import OpenAI from 'openai'
|
|||||||
import { CompletionsParams } from '.'
|
import { CompletionsParams } from '.'
|
||||||
import BaseProvider from './BaseProvider'
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
type ReasoningEffort = 'low' | 'medium' | 'high'
|
|
||||||
|
|
||||||
export default class GeminiProvider extends BaseProvider {
|
export default class GeminiProvider extends BaseProvider {
|
||||||
private sdk: GoogleGenAI
|
private sdk: GoogleGenAI
|
||||||
|
|
||||||
@ -213,32 +211,25 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
* @param model - The model
|
* @param model - The model
|
||||||
* @returns The reasoning effort
|
* @returns The reasoning effort
|
||||||
*/
|
*/
|
||||||
private getReasoningEffort(assistant: Assistant, model: Model) {
|
private getBudgetToken(assistant: Assistant, model: Model) {
|
||||||
if (isGemini25ReasoningModel(model)) {
|
if (isGeminiReasoningModel(model)) {
|
||||||
const effortRatios: Record<ReasoningEffort, number> = {
|
// 检查thinking_budget是否明确设置
|
||||||
high: 1,
|
const thinkingBudget = assistant?.settings?.thinking_budget
|
||||||
medium: 0.5,
|
|
||||||
low: 0.2
|
// 如果thinking_budget是undefined,使用模型的默认行为
|
||||||
}
|
if (thinkingBudget === undefined) {
|
||||||
const effort = assistant?.settings?.reasoning_effort as ReasoningEffort
|
return {} // 返回空对象以使用模型默认值
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果thinking_budget是明确设置的值(包括0),使用该值
|
||||||
return {
|
return {
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
thinkingBudget: budgetTokens,
|
thinkingBudget: thinkingBudget,
|
||||||
includeThoughts: true
|
includeThoughts: true
|
||||||
} as ThinkingConfig
|
} as ThinkingConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +301,7 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
topP: assistant?.settings?.topP,
|
topP: assistant?.settings?.topP,
|
||||||
maxOutputTokens: maxTokens,
|
maxOutputTokens: maxTokens,
|
||||||
tools: tools,
|
tools: tools,
|
||||||
...this.getReasoningEffort(assistant, model),
|
...this.getBudgetToken(assistant, model),
|
||||||
...this.getCustomParameters(assistant)
|
...this.getCustomParameters(assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
|
||||||
import {
|
import {
|
||||||
getOpenAIWebSearchParams,
|
getOpenAIWebSearchParams,
|
||||||
isGrokReasoningModel,
|
isGrokReasoningModel,
|
||||||
isHunyuanSearchModel,
|
isHunyuanSearchModel,
|
||||||
isOpenAIoSeries,
|
|
||||||
isOpenAIWebSearch,
|
isOpenAIWebSearch,
|
||||||
isReasoningModel,
|
isReasoningModel,
|
||||||
isSupportedModel,
|
isSupportedModel,
|
||||||
|
isSupportedReasoningEffortModel,
|
||||||
|
isSupportedReasoningEffortOpenAIModel,
|
||||||
|
isSupportedThinkingTokenClaudeModel,
|
||||||
|
isSupportedThinkingTokenModel,
|
||||||
|
isSupportedThinkingTokenQwenModel,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isZhipuModel,
|
isZhipuModel
|
||||||
OPENAI_NO_SUPPORT_DEV_ROLE_MODELS
|
|
||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
@ -57,8 +59,6 @@ import {
|
|||||||
import { CompletionsParams } from '.'
|
import { CompletionsParams } from '.'
|
||||||
import BaseProvider from './BaseProvider'
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
type ReasoningEffort = 'low' | 'medium' | 'high'
|
|
||||||
|
|
||||||
export default class OpenAIProvider extends BaseProvider {
|
export default class OpenAIProvider extends BaseProvider {
|
||||||
private sdk: OpenAI
|
private sdk: OpenAI
|
||||||
|
|
||||||
@ -262,9 +262,26 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
if (isReasoningModel(model)) {
|
if (isReasoningModel(model)) {
|
||||||
if (model.provider === 'openrouter') {
|
if (model.provider === 'openrouter') {
|
||||||
return {
|
if (isSupportedReasoningEffortModel(model)) {
|
||||||
reasoning: {
|
return {
|
||||||
effort: assistant?.settings?.reasoning_effort
|
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 {
|
return {
|
||||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.id.includes('claude-3.7-sonnet') || model.id.includes('claude-3-7-sonnet')) {
|
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||||
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 {
|
return {
|
||||||
thinking: {
|
thinking: {
|
||||||
type: 'enabled',
|
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
|
const isEnabledWebSearch = assistant.enableWebSearch || !!assistant.webSearchProviderId
|
||||||
messages = addImageFileToContents(messages)
|
messages = addImageFileToContents(messages)
|
||||||
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
||||||
if (isOpenAIoSeries(model) && !OPENAI_NO_SUPPORT_DEV_ROLE_MODELS.includes(model.id)) {
|
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||||
systemMessage = {
|
systemMessage = {
|
||||||
role: 'developer',
|
role: 'developer',
|
||||||
content: `Formatting re-enabled${systemMessage ? '\n' + systemMessage.content : ''}`
|
content: `Formatting re-enabled${systemMessage ? '\n' + systemMessage.content : ''}`
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export type Assistant = {
|
|||||||
enableWebSearch?: boolean
|
enableWebSearch?: boolean
|
||||||
webSearchProviderId?: WebSearchProvider['id']
|
webSearchProviderId?: WebSearchProvider['id']
|
||||||
enableGenerateImage?: boolean
|
enableGenerateImage?: boolean
|
||||||
|
enableThinking?: boolean
|
||||||
mcpServers?: MCPServer[]
|
mcpServers?: MCPServer[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ export type AssistantSettings = {
|
|||||||
defaultModel?: Model
|
defaultModel?: Model
|
||||||
customParameters?: AssistantSettingCustomParameters[]
|
customParameters?: AssistantSettingCustomParameters[]
|
||||||
reasoning_effort?: 'low' | 'medium' | 'high'
|
reasoning_effort?: 'low' | 'medium' | 'high'
|
||||||
|
thinking_budget?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Agent = Omit<Assistant, 'model'> & {
|
export type Agent = Omit<Assistant, 'model'> & {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user