From 084cedf071e4cbf410f64582d98830e1f2361614 Mon Sep 17 00:00:00 2001 From: SuYao Date: Mon, 19 May 2025 20:18:16 +0800 Subject: [PATCH] hotfix: add OpenAI settings tab and related functionality (#6040) * feat: add OpenAI settings tab and related functionality * fix: update related logic to support flexible service layer. * fix(OpenAIResponseProvider): remove unused isOpenAILLMModel import --- src/renderer/src/config/models.ts | 14 ++ src/renderer/src/i18n/locales/en-us.json | 14 ++ src/renderer/src/i18n/locales/ja-jp.json | 16 +- src/renderer/src/i18n/locales/ru-ru.json | 18 ++- src/renderer/src/i18n/locales/zh-cn.json | 14 ++ src/renderer/src/i18n/locales/zh-tw.json | 14 ++ .../src/pages/home/Tabs/OpenAISettingsTab.tsx | 144 ++++++++++++++++++ .../src/pages/home/Tabs/SettingsTab.tsx | 24 ++- .../providers/AiProvider/OpenAIProvider.ts | 1 + .../AiProvider/OpenAIResponseProvider.ts | 37 +++-- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 11 ++ src/renderer/src/store/settings.ts | 30 +++- src/renderer/src/types/index.ts | 3 + 14 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 5f7e3362ae..ee44a99608 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -2245,6 +2245,20 @@ export function isOpenAILLMModel(model: Model): boolean { return false } +export function isOpenAIModel(model: Model): boolean { + if (!model) { + return false + } + return model.id.includes('gpt') || isOpenAIReasoningModel(model) +} + +export function isSupportedFlexServiceTier(model: Model): boolean { + if (!model) { + return false + } + return (model.id.includes('o3') && !model.id.includes('o3-mini')) || model.id.includes('o4-mini') +} + export function isSupportedReasoningEffortOpenAIModel(model: Model): boolean { return ( (model.id.includes('o1') && !(model.id.includes('o1-preview') || model.id.includes('o1-mini'))) || diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fe9ce8e793..d3ba296a58 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1637,6 +1637,20 @@ "zoom": { "title": "Page Zoom", "reset": "Reset" + }, + "openai": { + "title": "OpenAI Settings", + "summary_text_mode.title": "Summary Mode", + "summary_text_mode.tip": "A summary of the reasoning performed by the model", + "summary_text_mode.auto": "auto", + "summary_text_mode.concise": "concise", + "summary_text_mode.detailed": "detailed", + "summary_text_mode.off": "off", + "service_tier.title": "Service Tier", + "service_tier.tip": "Specifies the latency tier to use for processing the request", + "service_tier.auto": "auto", + "service_tier.default": "default", + "service_tier.flex": "flex" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index c1f6b994c0..9e20973ca6 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1637,7 +1637,21 @@ }, "input.show_translate_confirm": "翻訳確認ダイアログを表示", "about.debug.title": "デバッグ", - "about.debug.open": "開く" + "about.debug.open": "開く", + "openai": { + "title": "OpenAIの設定", + "summary_text_mode.title": "要約モード", + "summary_text_mode.tip": "モデルが行った推論の要約", + "summary_text_mode.auto": "自動", + "summary_text_mode.concise": "簡潔", + "summary_text_mode.detailed": "詳細", + "summary_text_mode.off": "オフ", + "service_tier.title": "サービスティア", + "service_tier.tip": "リクエスト処理に使用するレイテンシティアを指定します", + "service_tier.auto": "自動", + "service_tier.default": "デフォルト", + "service_tier.flex": "フレックス" + } }, "translate": { "any.language": "任意の言語", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 2d4d179f34..f24f3510f7 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1636,8 +1636,22 @@ "reset": "Сбросить" }, "input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода", - "about.debug.title": "[to be translated]:Debug", - "about.debug.open": "[to be translated]:Open" + "openai": { + "title": "Настройки OpenAI", + "summary_text_mode.title": "Режим резюме", + "summary_text_mode.tip": "Резюме рассуждений, выполненных моделью", + "summary_text_mode.auto": "Авто", + "summary_text_mode.concise": "Краткий", + "summary_text_mode.detailed": "Подробный", + "summary_text_mode.off": "Выключен", + "service_tier.title": "Уровень сервиса", + "service_tier.tip": "Указывает уровень задержки, который следует использовать для обработки запроса", + "service_tier.auto": "Авто", + "service_tier.default": "По умолчанию", + "service_tier.flex": "Гибкий" + }, + "about.debug.title": "Отладка", + "about.debug.open": "Открыть" }, "translate": { "any.language": "Любой язык", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9ac0fd8807..ee836bea3b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1637,6 +1637,20 @@ "zoom": { "title": "缩放", "reset": "重置" + }, + "openai": { + "title": "OpenAI设置", + "summary_text_mode.title": "摘要模式", + "summary_text_mode.tip": "模型执行的推理摘要", + "summary_text_mode.auto": "自动", + "summary_text_mode.concise": "简洁", + "summary_text_mode.detailed": "详细", + "summary_text_mode.off": "关闭", + "service_tier.title": "服务层级", + "service_tier.tip": "指定用于处理请求的延迟层级", + "service_tier.auto": "自动", + "service_tier.default": "默认", + "service_tier.flex": "灵活" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 1dd2ec09dc..eb70159620 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1637,6 +1637,20 @@ "zoom": { "title": "縮放", "reset": "重置" + }, + "openai": { + "title": "OpenAI設定", + "summary_text_mode.title": "摘要模式", + "summary_text_mode.tip": "模型所執行的推理摘要", + "summary_text_mode.auto": "自動", + "summary_text_mode.concise": "簡潔", + "summary_text_mode.detailed": "詳細", + "summary_text_mode.off": "關閉", + "service_tier.title": "服務層級", + "service_tier.tip": "指定用於處理請求的延遲層級", + "service_tier.auto": "自動", + "service_tier.default": "預設", + "service_tier.flex": "彈性" } }, "translate": { diff --git a/src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx b/src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx new file mode 100644 index 0000000000..0fdd702d26 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx @@ -0,0 +1,144 @@ +import { SettingDivider, SettingRow, SettingSubtitle } from '@renderer/pages/settings' +import { RootState, useAppDispatch } from '@renderer/store' +import { setOpenAIServiceTier, setOpenAISummaryText } from '@renderer/store/settings' +import { OpenAIServiceTier, OpenAISummaryText } from '@renderer/types' +import { Select, Tooltip } from 'antd' +import { CircleHelp } from 'lucide-react' +import { FC, useCallback, useEffect, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { SettingGroup, SettingRowTitleSmall } from './SettingsTab' + +interface Props { + isOpenAIReasoning: boolean + isSupportedFlexServiceTier: boolean +} + +const FALL_BACK_SERVICE_TIER: Record = { + auto: 'auto', + default: 'default', + flex: 'default' +} + +const OpenAISettingsTab: FC = (props) => { + const { t } = useTranslation() + const summaryText = useSelector((state: RootState) => state.settings.openAI.summaryText) + const serviceTierMode = useSelector((state: RootState) => state.settings.openAI.serviceTier) + const dispatch = useAppDispatch() + + const setSummaryText = useCallback( + (value: OpenAISummaryText) => { + dispatch(setOpenAISummaryText(value)) + }, + [dispatch] + ) + + const setServiceTierMode = useCallback( + (value: OpenAIServiceTier) => { + dispatch(setOpenAIServiceTier(value)) + }, + [dispatch] + ) + + const summaryTextOptions = [ + { + value: 'auto', + label: t('settings.openai.summary_text_mode.auto') + }, + { + value: 'detailed', + label: t('settings.openai.summary_text_mode.detailed') + }, + { + value: 'off', + label: t('settings.openai.summary_text_mode.off') + } + ] + + const serviceTierOptions = useMemo(() => { + const baseOptions = [ + { + value: 'auto', + label: t('settings.openai.service_tier.auto') + }, + { + value: 'default', + label: t('settings.openai.service_tier.default') + }, + { + value: 'flex', + label: t('settings.openai.service_tier.flex') + } + ] + return baseOptions.filter((option) => { + if (option.value === 'flex') { + return props.isSupportedFlexServiceTier + } + return true + }) + }, [props.isSupportedFlexServiceTier, t]) + + useEffect(() => { + if (serviceTierMode && !serviceTierOptions.some((option) => option.value === serviceTierMode)) { + setServiceTierMode(FALL_BACK_SERVICE_TIER[serviceTierMode]) + } + }, [serviceTierMode, serviceTierOptions, setServiceTierMode]) + + return ( + + {t('settings.openai.title')} + + + + {t('settings.openai.service_tier.title')}{' '} + + + + + { + setServiceTierMode(value as OpenAIServiceTier) + }} + size="small" + options={serviceTierOptions} + /> + + {props.isOpenAIReasoning && ( + <> + + + + {t('settings.openai.summary_text_mode.title')}{' '} + + + + + { + setSummaryText(value as OpenAISummaryText) + }} + size="small" + options={summaryTextOptions} + /> + + + )} + + ) +} + +const StyledSelect = styled(Select)` + .ant-select-selector { + border-radius: 15px !important; + padding: 4px 10px !important; + height: 26px !important; + } +` + +export default OpenAISettingsTab diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 06bd5e335b..7a72a43147 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -8,11 +8,18 @@ import { isMac, isWindows } from '@renderer/config/constant' +import { + isOpenAIModel, + isSupportedFlexServiceTier, + isSupportedReasoningEffortOpenAIModel +} from '@renderer/config/models' import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider' import { useAssistant } from '@renderer/hooks/useAssistant' +import { useProvider } from '@renderer/hooks/useProvider' import { useSettings } from '@renderer/hooks/useSettings' import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' +import { getDefaultModel } from '@renderer/services/AssistantService' import { useAppDispatch } from '@renderer/store' import { SendMessageShortcut, @@ -57,12 +64,15 @@ import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import OpenAISettingsTab from './OpenAISettingsTab' + interface Props { assistant: Assistant } const SettingsTab: FC = (props) => { const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id) + const { provider } = useProvider(assistant.model.provider) const { messageStyle, codeStyle, fontSize, language } = useSettings() const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) @@ -155,6 +165,15 @@ const SettingsTab: FC = (props) => { const assistantContextCount = assistant?.settings?.contextCount || 20 const maxContextCount = assistantContextCount > 20 ? assistantContextCount : 20 + const model = assistant.model || getDefaultModel() + + const isOpenAI = isOpenAIModel(model) + const isOpenAIReasoning = + isSupportedReasoningEffortOpenAIModel(model) && + !model.id.includes('o1-pro') && + (provider.type === 'openai-response' || provider.id === 'aihubmix') + const isOpenAIFlexServiceTier = isSupportedFlexServiceTier(model) + return ( @@ -265,6 +284,9 @@ const SettingsTab: FC = (props) => { )} + {isOpenAI && ( + + )} {t('settings.messages.title')} @@ -629,7 +651,7 @@ const Label = styled.p` margin-right: 5px; ` -const SettingRowTitleSmall = styled(SettingRowTitle)` +export const SettingRowTitleSmall = styled(SettingRowTitle)` font-size: 13px; ` diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 7740065c43..3723474d96 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -476,6 +476,7 @@ export default class OpenAIProvider extends BaseOpenAIProvider { keep_alive: this.keepAliveTime, stream: isSupportStreamOutput(), tools: !isEmpty(tools) ? tools : undefined, + service_tier: this.getServiceTier(model), ...getOpenAIWebSearchParams(assistant, model), ...this.getReasoningEffort(assistant, model), ...this.getProviderSpecificParameters(assistant, model), diff --git a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts index e261784452..43289aca9c 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts @@ -1,7 +1,8 @@ import { - isOpenAILLMModel, + isOpenAIModel, isOpenAIReasoningModel, isOpenAIWebSearch, + isSupportedFlexServiceTier, isSupportedModel, isSupportedReasoningEffortOpenAIModel, isVisionModel @@ -25,6 +26,8 @@ import { MCPToolResponse, Metrics, Model, + OpenAIServiceTier, + OpenAISummaryText, Provider, Suggestion, ToolCallResponse, @@ -176,17 +179,25 @@ export abstract class BaseOpenAIProvider extends BaseProvider { } protected getServiceTier(model: Model) { - if ((model.id.includes('o3') && !model.id.includes('o3-mini')) || model.id.includes('o4-mini')) { - return 'flex' + if (!isOpenAIModel(model)) return undefined + const openAI = getStoreSetting('openAI') as any + let serviceTier = 'auto' as OpenAIServiceTier + + if (openAI.serviceTier === 'flex') { + if (isSupportedFlexServiceTier(model)) { + serviceTier = 'flex' + } else { + serviceTier = 'auto' + } + } else { + serviceTier = openAI.serviceTier } - if (isOpenAILLMModel(model)) { - return 'auto' - } - return undefined + + return serviceTier } protected getTimeout(model: Model) { - if ((model.id.includes('o3') && !model.id.includes('o3-mini')) || model.id.includes('o4-mini')) { + if (isSupportedFlexServiceTier(model)) { return 15 * 1000 * 60 } return 5 * 1000 * 60 @@ -196,6 +207,14 @@ export abstract class BaseOpenAIProvider extends BaseProvider { if (!isSupportedReasoningEffortOpenAIModel(model)) { return {} } + const openAI = getStoreSetting('openAI') as any + const summaryText = openAI.summaryText as OpenAISummaryText + let summary: string | undefined = undefined + if (summaryText === 'off' || model.id.includes('o1-pro')) { + summary = undefined + } else { + summary = summaryText + } const reasoningEffort = assistant?.settings?.reasoning_effort if (!reasoningEffort) { @@ -206,7 +225,7 @@ export abstract class BaseOpenAIProvider extends BaseProvider { return { reasoning: { effort: reasoningEffort as OpenAI.ReasoningEffort, - summary: 'detailed' + summary: summary } as OpenAI.Reasoning } } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index abfc334986..afa1a1da01 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -46,7 +46,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 100, + version: 102, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 029b05125c..9efe86c7b5 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1343,6 +1343,17 @@ const migrateConfig = { } catch (error) { return state } + }, + '102': (state: RootState) => { + try { + state.settings.openAI = { + summaryText: 'off', + serviceTier: 'auto' + } + return state + } catch (error) { + return state + } } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 4dcc7203a8..f756401507 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -1,6 +1,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { TRANSLATE_PROMPT } from '@renderer/config/prompts' -import { CodeStyleVarious, LanguageVarious, MathEngine, ThemeMode, TranslateLanguageVarious } from '@renderer/types' +import { + CodeStyleVarious, + LanguageVarious, + MathEngine, + OpenAIServiceTier, + OpenAISummaryText, + ThemeMode, + TranslateLanguageVarious +} from '@renderer/types' import { WebDAVSyncState } from './backup' @@ -132,6 +140,11 @@ export interface SettingsState { siyuan: boolean docx: boolean } + // OpenAI + openAI: { + summaryText: OpenAISummaryText + serviceTier: OpenAIServiceTier + } } export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid' @@ -238,6 +251,11 @@ export const initialState: SettingsState = { obsidian: true, siyuan: true, docx: true + }, + // OpenAI + openAI: { + summaryText: 'off', + serviceTier: 'auto' } } @@ -519,6 +537,12 @@ const settingsSlice = createSlice({ }, setEnableBackspaceDeleteModel: (state, action: PayloadAction) => { state.enableBackspaceDeleteModel = action.payload + }, + setOpenAISummaryText: (state, action: PayloadAction) => { + state.openAI.summaryText = action.payload + }, + setOpenAIServiceTier: (state, action: PayloadAction) => { + state.openAI.serviceTier = action.payload } } }) @@ -613,7 +637,9 @@ export const { setEnableDataCollection, setEnableQuickPanelTriggers, setExportMenuOptions, - setEnableBackspaceDeleteModel + setEnableBackspaceDeleteModel, + setOpenAISummaryText, + setOpenAIServiceTier } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 0169bfd83f..52b12923b7 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -653,3 +653,6 @@ export interface StoreSyncAction { source?: string } } + +export type OpenAISummaryText = 'auto' | 'concise' | 'detailed' | 'off' +export type OpenAIServiceTier = 'auto' | 'default' | 'flex'