feat: Add new model type for reasoning models & reasoning_effort setting (#992)

This commit is contained in:
Gutsy Yuan 2025-02-05 16:15:31 +08:00 committed by GitHub
parent 9024d48938
commit 9acae0a728
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 93 additions and 19 deletions

View File

@ -1,4 +1,4 @@
import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { isEmbeddingModel, isReasoningModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { isFreeModel } from '@renderer/utils' import { isFreeModel } from '@renderer/utils'
import { Tag } from 'antd' import { Tag } from 'antd'
@ -29,6 +29,11 @@ const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true }) => {
{t('models.embedding')} {t('models.embedding')}
</Tag> </Tag>
)} )}
{isReasoningModel(model) && (
<Tag color="blue" style={{ marginLeft: 10 }}>
{t('models.reasoning')}
</Tag>
)}
</> </>
) )
} }

View File

@ -156,6 +156,8 @@ export const VISION_REGEX = new RegExp(
) )
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\breasoner\b.*|.*-[rR]\d+.*)$/i
export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
@ -1127,6 +1129,14 @@ 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 isReasoningModel(model: Model): boolean {
if (!model) {
return false
}
return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false
}
export function isSupportedModel(model: OpenAI.Models.Model): boolean { export function isSupportedModel(model: OpenAI.Models.Model): boolean {
if (!model) { if (!model) {
return false return false

View File

@ -40,6 +40,11 @@
"save.success": "Saved successfully", "save.success": "Saved successfully",
"save.title": "Save to agent", "save.title": "Save to agent",
"search": "Search assistants...", "search": "Search assistants...",
"settings.reasoning_effort": "Reasoning effort",
"settings.reasoning_effort.tip": "Only supports reasoning models",
"settings.reasoning_effort.low": "low",
"settings.reasoning_effort.medium": "medium",
"settings.reasoning_effort.high": "high",
"settings.auto_reset_model": "Auto Reset Model", "settings.auto_reset_model": "Auto Reset Model",
"settings.auto_reset_model.tip": "Automatically reset the model when a new topic is created.", "settings.auto_reset_model.tip": "Automatically reset the model when a new topic is created.",
"settings.default_model": "Default Model", "settings.default_model": "Default Model",
@ -640,12 +645,14 @@
"select": "Select Model Types", "select": "Select Model Types",
"text": "Text", "text": "Text",
"vision": "Vision", "vision": "Vision",
"embedding": "Embedding" "embedding": "Embedding",
"reasoning": "Reasoning"
}, },
"all": "All", "all": "All",
"vision": "Vision", "vision": "Vision",
"websearch": "WebSearch", "websearch": "WebSearch",
"free": "Free", "free": "Free",
"reasoning": "Reasoning",
"embedding": "Embedding", "embedding": "Embedding",
"embedding_model": "Embedding Model", "embedding_model": "Embedding Model",
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage", "embedding_model_tooltip": "Add in Settings->Model Provider->Manage",

View File

@ -625,12 +625,14 @@
"select": "モデルタイプを選択", "select": "モデルタイプを選択",
"text": "テキスト", "text": "テキスト",
"vision": "画像", "vision": "画像",
"embedding": "埋め込み" "embedding": "埋め込み",
"reasoning": "推論"
}, },
"all": "すべて", "all": "すべて",
"vision": "画像モデル", "vision": "画像モデル",
"websearch": "ウェブ検索モデル", "websearch": "ウェブ検索モデル",
"free": "無料モデル", "free": "無料モデル",
"reasoning": "推論モデル",
"embedding": "埋め込みモデル", "embedding": "埋め込みモデル",
"embedding_model": "埋め込みモデル", "embedding_model": "埋め込みモデル",
"embedding_model_tooltip": "設定->モデルサービス->管理で追加", "embedding_model_tooltip": "設定->モデルサービス->管理で追加",

View File

@ -637,12 +637,14 @@
"select": "Выберите тип модели", "select": "Выберите тип модели",
"text": "Текст", "text": "Текст",
"vision": "Изображение", "vision": "Изображение",
"embedding": "Встраиваемые" "embedding": "Встраиваемые",
"reasoning": "Рассуждение"
}, },
"all": "Все", "all": "Все",
"vision": "Визуальные модели", "vision": "Визуальные модели",
"websearch": "Веб-поисковые модели", "websearch": "Веб-поисковые модели",
"free": "Бесплатные модели", "free": "Бесплатные модели",
"reasoning": "Модели рассуждения",
"embedding": "Встраиваемые модели", "embedding": "Встраиваемые модели",
"embedding_model": "Встраиваемые модели", "embedding_model": "Встраиваемые модели",
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление", "embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",

View File

@ -40,6 +40,11 @@
"save.success": "保存成功", "save.success": "保存成功",
"save.title": "保存到智能体", "save.title": "保存到智能体",
"search": "搜索助手", "search": "搜索助手",
"settings.reasoning_effort": "思维链长度",
"settings.reasoning_effort.tip": "该设置仅支持推理模型",
"settings.reasoning_effort.low": "短",
"settings.reasoning_effort.medium": "中",
"settings.reasoning_effort.high": "长",
"settings.auto_reset_model": "自动重置模型", "settings.auto_reset_model": "自动重置模型",
"settings.auto_reset_model.tip": "创建新话题时自动重置模型", "settings.auto_reset_model.tip": "创建新话题时自动重置模型",
"settings.default_model": "默认模型", "settings.default_model": "默认模型",
@ -627,12 +632,14 @@
"select": "选择模型类型", "select": "选择模型类型",
"text": "文本", "text": "文本",
"vision": "图像", "vision": "图像",
"embedding": "嵌入" "embedding": "嵌入",
"reasoning": "推理"
}, },
"all": "全部", "all": "全部",
"vision": "视觉模型", "vision": "视觉模型",
"websearch": "联网模型", "websearch": "联网模型",
"free": "免费模型", "free": "免费模型",
"reasoning": "推理模型",
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",

View File

@ -40,6 +40,11 @@
"save.success": "儲存成功", "save.success": "儲存成功",
"save.title": "儲存到智能體", "save.title": "儲存到智能體",
"search": "搜尋助手...", "search": "搜尋助手...",
"settings.reasoning_effort": "思維鏈長度",
"settings.reasoning_effort.tip": "該設置僅支持推理模型",
"settings.reasoning_effort.low": "短",
"settings.reasoning_effort.medium": "中",
"settings.reasoning_effort.high": "長",
"settings.auto_reset_model": "自動重置模型", "settings.auto_reset_model": "自動重置模型",
"settings.auto_reset_model.tip": "每次新的話題時自動重置模型", "settings.auto_reset_model.tip": "每次新的話題時自動重置模型",
"settings.default_model": "預設模型", "settings.default_model": "預設模型",
@ -626,12 +631,14 @@
"select": "選擇模型類型", "select": "選擇模型類型",
"text": "文字", "text": "文字",
"vision": "圖像", "vision": "圖像",
"embedding": "嵌入" "embedding": "嵌入",
"reasoning": "推理"
}, },
"all": "全部", "all": "全部",
"vision": "視覺模型", "vision": "視覺模型",
"websearch": "網路搜索模型", "websearch": "網路搜索模型",
"free": "免費模型", "free": "免費模型",
"reasoning": "推理模型",
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",

View File

@ -5,7 +5,7 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
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 { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd' import { Button, Col, Divider, Input, InputNumber, Radio, 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,6 +23,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
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 [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false) const [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort ?? 'medium')
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,6 +44,10 @@ 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 })
@ -384,6 +389,26 @@ 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.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}>

View File

@ -9,7 +9,7 @@ import {
} from '@ant-design/icons' } from '@ant-design/icons'
import ModelTags from '@renderer/components/ModelTags' import ModelTags from '@renderer/components/ModelTags'
import OAuthButton from '@renderer/components/OAuth/OAuthButton' import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { EMBEDDING_REGEX, getModelLogo, VISION_REGEX } from '@renderer/config/models' import { EMBEDDING_REGEX, getModelLogo, REASONING_REGEX, VISION_REGEX } from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers' import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
@ -187,7 +187,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
onChange={(types) => onUpdateModelTypes(model, types as ModelType[])} onChange={(types) => onUpdateModelTypes(model, types as ModelType[])}
options={[ options={[
{ label: t('models.type.vision'), value: 'vision', disabled: VISION_REGEX.test(model.id) }, { label: t('models.type.vision'), value: 'vision', disabled: VISION_REGEX.test(model.id) },
{ label: t('models.type.embedding'), value: 'embedding', disabled: EMBEDDING_REGEX.test(model.id) } { label: t('models.type.embedding'), value: 'embedding', disabled: EMBEDDING_REGEX.test(model.id) },
{ label: t('models.type.reasoning'), value: 'reasoning', disabled: REASONING_REGEX.test(model.id) }
]} ]}
/> />
</div> </div>

View File

@ -1,4 +1,4 @@
import { getOpenAIWebSearchParams, isSupportedModel, isVisionModel } from '@renderer/config/models' import { getOpenAIWebSearchParams, isReasoningModel, isSupportedModel, isVisionModel } 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'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService' import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
@ -118,13 +118,7 @@ export default class OpenAIProvider extends BaseProvider {
} }
private getTemperature(assistant: Assistant, model: Model) { private getTemperature(assistant: Assistant, model: Model) {
if (model.id.startsWith('o1') || model.id.startsWith('o3')) { if (isReasoningModel(model)) return undefined
return undefined
}
if (model.provider === 'deepseek' && model.id === 'deepseek-reasoner') {
return undefined
}
return assistant?.settings?.temperature return assistant?.settings?.temperature
} }
@ -141,6 +135,18 @@ export default class OpenAIProvider extends BaseProvider {
return {} return {}
} }
private getTopP(assistant: Assistant, model: Model) {
if (isReasoningModel(model)) return undefined
return assistant?.settings?.topP
}
private getReasoningEffort(assistant: Assistant, model: Model) {
if (isReasoningModel(model)) return assistant?.settings?.reasoning_effort
return undefined
}
async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> { async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
@ -182,10 +188,11 @@ export default class OpenAIProvider extends BaseProvider {
Boolean Boolean
) as ChatCompletionMessageParam[], ) as ChatCompletionMessageParam[],
temperature: this.getTemperature(assistant, model), temperature: this.getTemperature(assistant, model),
top_p: assistant?.settings?.topP, top_p: this.getTopP(assistant, model),
max_tokens: maxTokens, max_tokens: maxTokens,
keep_alive: this.keepAliveTime, keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput(), stream: isSupportStreamOutput(),
reasoning_effort: this.getReasoningEffort(assistant, model),
...(assistant.enableWebSearch ? getOpenAIWebSearchParams(model) : {}), ...(assistant.enableWebSearch ? getOpenAIWebSearchParams(model) : {}),
...this.getProviderSpecificParameters(model), ...this.getProviderSpecificParameters(model),
...this.getCustomParameters(assistant) ...this.getCustomParameters(assistant)

View File

@ -38,6 +38,7 @@ export type AssistantSettings = {
defaultModel?: Model defaultModel?: Model
autoResetModel: boolean autoResetModel: boolean
customParameters?: AssistantSettingCustomParameters[] customParameters?: AssistantSettingCustomParameters[]
reasoning_effort?: 'low' | 'medium' | 'high'
} }
export type Agent = Omit<Assistant, 'model'> export type Agent = Omit<Assistant, 'model'>
@ -107,7 +108,7 @@ export type Provider = {
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
export type ModelType = 'text' | 'vision' | 'embedding' export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning'
export type Model = { export type Model = {
id: string id: string