diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index aef37943b7..47e7d2510c 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -14,6 +14,7 @@ import { } from '@renderer/config/models' import { mapLanguageToQwenMTModel } from '@renderer/config/translate' import { getStoreSetting } from '@renderer/hooks/useSettings' +import { getProviderById } from '@renderer/services/ProviderService' import { type Assistant, type GroqServiceTier, @@ -31,7 +32,7 @@ import { type ServiceTier } from '@renderer/types' import type { OpenAIVerbosity } from '@renderer/types/aiCoreTypes' -import { isSupportServiceTierProvider } from '@renderer/utils/provider' +import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@renderer/utils/provider' import type { JSONValue } from 'ai' import { t } from 'i18next' @@ -248,8 +249,13 @@ function buildOpenAIProviderOptions( ...reasoningParams } } + const provider = getProviderById(model.provider) - if (isSupportVerbosityModel(model)) { + if (!provider) { + throw new Error(`Provider ${model.provider} not found`) + } + + if (isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider)) { const openAI = getStoreSetting<'openAI'>('openAI') const userVerbosity = openAI?.verbosity diff --git a/src/renderer/src/components/Selector.tsx b/src/renderer/src/components/Selector.tsx index 38567fc200..e30bc64193 100644 --- a/src/renderer/src/components/Selector.tsx +++ b/src/renderer/src/components/Selector.tsx @@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' -interface SelectorOption { +interface SelectorOption { label: string | ReactNode value: V type?: 'group' @@ -14,7 +14,7 @@ interface SelectorOption { disabled?: boolean } -interface BaseSelectorProps { +interface BaseSelectorProps { options: SelectorOption[] placeholder?: string placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom' @@ -39,7 +39,7 @@ interface MultipleSelectorProps extends BaseSelectorProps { export type SelectorProps = SingleSelectorProps | MultipleSelectorProps -const Selector = ({ +const Selector = ({ options, value, onChange = () => {}, diff --git a/src/renderer/src/config/models/utils.ts b/src/renderer/src/config/models/utils.ts index dabe9ff409..61b994a916 100644 --- a/src/renderer/src/config/models/utils.ts +++ b/src/renderer/src/config/models/utils.ts @@ -19,6 +19,7 @@ export function isSupportFlexServiceTierModel(model: Model): boolean { (modelId.includes('o3') && !modelId.includes('o3-mini')) || modelId.includes('o4-mini') || modelId.includes('gpt-5') ) } + export function isSupportedFlexServiceTier(model: Model): boolean { return isSupportFlexServiceTierModel(model) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 329ec7879b..92ad9a8b87 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1148,6 +1148,7 @@ "fullscreen": "Entered fullscreen mode. Press F11 to exit", "go_to_settings": "Go to settings", "i_know": "I know", + "ignore": "Ignore", "inspect": "Inspect", "invalid_value": "Invalid Value", "knowledge_base": "Knowledge Base", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "View WebDAV settings" }, + "groq": { + "title": "Groq Settings" + }, "hardware_acceleration": { "confirm": { "content": "Disabling hardware acceleration requires restarting the app to take effect. Do you want to restart now?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "Does the provider support the stream_options parameter?", "label": "Support stream_options" + }, + "verbosity": { + "help": "Whether the provider supports the verbosity parameter", + "label": "Support verbosity" } }, "url": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0d44039a16..440d7bd562 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1148,6 +1148,7 @@ "fullscreen": "已进入全屏模式,按 F11 退出", "go_to_settings": "前往设置", "i_know": "我知道了", + "ignore": "忽略", "inspect": "检查", "invalid_value": "无效值", "knowledge_base": "知识库", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "查看 WebDAV 设置" }, + "groq": { + "title": "Groq 设置" + }, "hardware_acceleration": { "confirm": { "content": "禁用硬件加速需要重启应用才能生效,是否现在重启?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "该提供商是否支持 stream_options 参数", "label": "支持 stream_options" + }, + "verbosity": { + "help": "该提供商是否支持 verbosity 参数", + "label": "支持 verbosity" } }, "url": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 58f036ce7f..6b201db319 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1148,6 +1148,7 @@ "fullscreen": "已進入全螢幕模式,按 F11 結束", "go_to_settings": "前往設定", "i_know": "我知道了", + "ignore": "忽略", "inspect": "檢查", "invalid_value": "無效值", "knowledge_base": "知識庫", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "檢視 WebDAV 設定" }, + "groq": { + "title": "Groq 設定" + }, "hardware_acceleration": { "confirm": { "content": "禁用硬件加速需要重新啟動應用程序才能生效。是否立即重新啟動?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "該提供商是否支援 stream_options 參數", "label": "支援 stream_options" + }, + "verbosity": { + "help": "提供者是否支援詳細程度參數", + "label": "支援詳細資訊" } }, "url": { diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 4cdadd638e..a1d5bb3c1a 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -1148,6 +1148,7 @@ "fullscreen": "Vollbildmodus aktiviert, F11 zum Beenden", "go_to_settings": "Zu Einstellungen", "i_know": "Verstanden", + "ignore": "Ignorieren", "inspect": "Prüfen", "invalid_value": "Ungültiger Wert", "knowledge_base": "Wissensdatenbank", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "WebDAV-Einstellungen anzeigen" }, + "groq": { + "title": "Groq Einstellungen" + }, "hardware_acceleration": { "confirm": { "content": "Deaktivierung der Hardwarebeschleunigung erfordert Neustart. Jetzt neu starten?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "Unterstützt stream_options", "label": "Unterstützt stream_options" + }, + "verbosity": { + "help": "Ob der Anbieter den Ausführlichkeitsparameter unterstützt", + "label": "Unterstützung der Ausführlichkeit" } }, "url": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 5175611ba3..2e02207c91 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1148,6 +1148,7 @@ "fullscreen": "Εισήχθη σε πλήρη οθόνη, πατήστε F11 για να έξω", "go_to_settings": "Πηγαίνετε στις ρυθμίσεις", "i_know": "Το έχω καταλάβει", + "ignore": "Αγνόησε", "inspect": "Επιθεώρηση", "invalid_value": "Μη έγκυρη τιμή", "knowledge_base": "Βάση Γνώσεων", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "Προβολή ρυθμίσεων WebDAV" }, + "groq": { + "title": "Ρυθμίσεις Groq" + }, "hardware_acceleration": { "confirm": { "content": "Η απενεργοποίηση της υλικοποιημένης επιτάχυνσης απαιτεί επανεκκίνηση της εφαρμογής για να τεθεί σε ισχύ. Θέλετε να επανεκκινήσετε τώρα;", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "Υποστηρίζει ο πάροχος την παράμετρο stream_options;", "label": "Υποστήριξη stream_options" + }, + "verbosity": { + "help": "Αν ο πάροχος υποστηρίζει την παράμετρο αναλυτικότητας", + "label": "Υποστήριξη πολυλογίας" } }, "url": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 9b3923cf94..39b5cb1248 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1148,6 +1148,7 @@ "fullscreen": "En modo pantalla completa, presione F11 para salir", "go_to_settings": "Ir a la configuración", "i_know": "Entendido", + "ignore": "Ignorar", "inspect": "Inspeccionar", "invalid_value": "Valor inválido", "knowledge_base": "Base de conocimiento", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "Ver configuración WebDAV" }, + "groq": { + "title": "Configuración de Groq" + }, "hardware_acceleration": { "confirm": { "content": "La desactivación de la aceleración por hardware requiere reiniciar la aplicación para que surta efecto, ¿desea reiniciar ahora?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "¿Admite el proveedor el parámetro stream_options?", "label": "Admite stream_options" + }, + "verbosity": { + "help": "Si el proveedor admite el parámetro de verbosidad", + "label": "Soporte de verbosidad" } }, "url": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 8212e27879..d1f3681e5e 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1148,6 +1148,7 @@ "fullscreen": "Mode plein écran, appuyez sur F11 pour quitter", "go_to_settings": "Aller aux paramètres", "i_know": "J'ai compris", + "ignore": "Ignorer", "inspect": "Vérifier", "invalid_value": "valeur invalide", "knowledge_base": "Base de connaissances", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "Voir les paramètres WebDAV" }, + "groq": { + "title": "Paramètres Groq" + }, "hardware_acceleration": { "confirm": { "content": "La désactivation de l'accélération matérielle nécessite un redémarrage de l'application pour prendre effet. Voulez-vous redémarrer maintenant ?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "Le fournisseur prend-il en charge le paramètre stream_options ?", "label": "Prise en charge des options de flux" + }, + "verbosity": { + "help": "Si le fournisseur prend en charge le paramètre de verbosité", + "label": "Prend en charge la verbosité" } }, "url": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 46817adcc1..639b17509d 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -1148,6 +1148,7 @@ "fullscreen": "全画面モードに入りました。F11キーで終了します", "go_to_settings": "設定に移動", "i_know": "わかりました", + "ignore": "無視", "inspect": "検査", "invalid_value": "無効な値", "knowledge_base": "ナレッジベース", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "WebDAV設定を表示" }, + "groq": { + "title": "Groq設定" + }, "hardware_acceleration": { "confirm": { "content": "ハードウェアアクセラレーションを無効にするには、アプリを再起動する必要があります。再起動しますか?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "このプロバイダーは stream_options パラメータをサポートしていますか", "label": "stream_options をサポート" + }, + "verbosity": { + "help": "プロバイダーが冗長度パラメータをサポートしているかどうか", + "label": "冗長性のサポート" } }, "url": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 805c2e8374..9fc5186c0e 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1148,6 +1148,7 @@ "fullscreen": "Entrou no modo de tela cheia, pressione F11 para sair", "go_to_settings": "Ir para configurações", "i_know": "Entendi", + "ignore": "Pular", "inspect": "Verificar", "invalid_value": "Valor inválido", "knowledge_base": "Base de Conhecimento", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "Ver configurações WebDAV" }, + "groq": { + "title": "Configurações do Groq" + }, "hardware_acceleration": { "confirm": { "content": "A desativação da aceleração de hardware requer a reinicialização do aplicativo para entrar em vigor. Deseja reiniciar agora?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "O fornecedor suporta o parâmetro stream_options?", "label": "suporta stream_options" + }, + "verbosity": { + "help": "Se o provedor suporta o parâmetro de verbosidade", + "label": "Suportar verbosidade" } }, "url": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 82bde21a89..899c97ef89 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -1148,6 +1148,7 @@ "fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода", "go_to_settings": "Перейти в настройки", "i_know": "Я понял", + "ignore": "Игнорировать", "inspect": "Осмотреть", "invalid_value": "недопустимое значение", "knowledge_base": "База знаний", @@ -3771,6 +3772,9 @@ }, "view_webdav_settings": "Просмотр настроек WebDAV" }, + "groq": { + "title": "Настройки Groq" + }, "hardware_acceleration": { "confirm": { "content": "Отключение аппаратного ускорения требует перезапуска приложения для вступления в силу. Перезапустить приложение?", @@ -4357,6 +4361,10 @@ "stream_options": { "help": "Поддерживает ли этот провайдер параметр stream_options", "label": "Поддержка stream_options" + }, + "verbosity": { + "help": "Поддерживает ли провайдер параметр verbosity", + "label": "Поддержка многословности" } }, "url": { diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 8b39e766fd..31dcfe437e 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -53,9 +53,10 @@ import { setThoughtAutoCollapse } from '@renderer/store/settings' import type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types' -import { ThemeMode } from '@renderer/types' +import { isGroqSystemProvider, ThemeMode } from '@renderer/types' import { modalConfirm } from '@renderer/utils' import { getSendMessageShortcutLabel } from '@renderer/utils/input' +import { isSupportServiceTierProvider } from '@renderer/utils/provider' import { Button, Col, InputNumber, Row, Slider, Switch } from 'antd' import { Settings2 } from 'lucide-react' import type { FC } from 'react' @@ -63,6 +64,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import GroqSettingsGroup from './components/GroqSettingsGroup' import OpenAISettingsGroup from './components/OpenAISettingsGroup' interface Props { @@ -181,7 +183,7 @@ const SettingsTab: FC = (props) => { const model = assistant.model || getDefaultModel() - const isOpenAI = isOpenAIModel(model) + const showOpenAiSettings = isOpenAIModel(model) || isSupportServiceTierProvider(provider) return ( @@ -332,7 +334,7 @@ const SettingsTab: FC = (props) => { )} - {isOpenAI && ( + {showOpenAiSettings && ( = (props) => { SettingRowTitleSmall={SettingRowTitleSmall} /> )} + {isGroqSystemProvider(provider) && ( + + )} diff --git a/src/renderer/src/pages/home/Tabs/components/GroqSettingsGroup.tsx b/src/renderer/src/pages/home/Tabs/components/GroqSettingsGroup.tsx new file mode 100644 index 0000000000..6e772e377f --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/GroqSettingsGroup.tsx @@ -0,0 +1,79 @@ +import Selector from '@renderer/components/Selector' +import { useProvider } from '@renderer/hooks/useProvider' +import { SettingDivider, SettingRow } from '@renderer/pages/settings' +import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup' +import type { GroqServiceTier, ServiceTier } from '@renderer/types' +import { SystemProviderIds } from '@renderer/types' +import { toOptionValue, toRealValue } from '@renderer/utils/select' +import { Tooltip } from 'antd' +import { CircleHelp } from 'lucide-react' +import type { FC } from 'react' +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +type ServiceTierOptions = { value: NonNullable | 'undefined'; label: string } + +interface Props { + SettingGroup: FC<{ children: React.ReactNode }> + SettingRowTitleSmall: FC<{ children: React.ReactNode }> +} + +const GroqSettingsGroup: FC = ({ SettingGroup, SettingRowTitleSmall }) => { + const { t } = useTranslation() + const { provider, updateProvider } = useProvider(SystemProviderIds.groq) + const serviceTierMode = provider.serviceTier + + const setServiceTierMode = useCallback( + (value: ServiceTier) => { + updateProvider({ serviceTier: value }) + }, + [updateProvider] + ) + + const serviceTierOptions = useMemo(() => { + const options = [ + { + value: 'undefined', + label: t('common.ignore') + }, + { + value: 'auto', + label: t('settings.openai.service_tier.auto') + }, + { + value: 'on_demand', + label: t('settings.openai.service_tier.on_demand') + }, + { + value: 'flex', + label: t('settings.openai.service_tier.flex') + } + ] as const satisfies ServiceTierOptions[] + return options + }, [t]) + + return ( + + + + + {t('settings.openai.service_tier.title')}{' '} + + + + + { + setServiceTierMode(toRealValue(value)) + }} + options={serviceTierOptions} + /> + + + + + ) +} + +export default GroqSettingsGroup diff --git a/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx index fac346261f..bf3e1970dc 100644 --- a/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx +++ b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx @@ -11,10 +11,11 @@ import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup' import type { RootState } from '@renderer/store' import { useAppDispatch } from '@renderer/store' import { setOpenAISummaryText, setOpenAIVerbosity } from '@renderer/store/settings' -import type { GroqServiceTier, Model, OpenAIServiceTier, ServiceTier } from '@renderer/types' -import { GroqServiceTiers, OpenAIServiceTiers, SystemProviderIds } from '@renderer/types' +import type { Model, OpenAIServiceTier, ServiceTier } from '@renderer/types' +import { SystemProviderIds } from '@renderer/types' import type { OpenAISummaryText, OpenAIVerbosity } from '@renderer/types/aiCoreTypes' -import { isSupportServiceTierProvider } from '@renderer/utils/provider' +import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@renderer/utils/provider' +import { toOptionValue, toRealValue } from '@renderer/utils/select' import { Tooltip } from 'antd' import { CircleHelp } from 'lucide-react' import type { FC } from 'react' @@ -23,19 +24,16 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' type VerbosityOption = { - value: OpenAIVerbosity + value: NonNullable | 'undefined' label: string } type SummaryTextOption = { - value: OpenAISummaryText + value: NonNullable | 'undefined' label: string } -type OpenAIServiceTierOption = { value: OpenAIServiceTier; label: string } -type GroqServiceTierOption = { value: GroqServiceTier; label: string } - -type ServiceTierOptions = OpenAIServiceTierOption[] | GroqServiceTierOption[] +type OpenAIServiceTierOption = { value: NonNullable | 'null' | 'undefined'; label: string } interface Props { model: Model @@ -52,13 +50,14 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti const serviceTierMode = provider.serviceTier const dispatch = useAppDispatch() - const isOpenAIReasoning = + const showSummarySetting = isSupportedReasoningEffortOpenAIModel(model) && !model.id.includes('o1-pro') && - (provider.type === 'openai-response' || provider.id === 'aihubmix') - const isSupportVerbosity = isSupportVerbosityModel(model) + (provider.type === 'openai-response' || model.endpoint_type === 'openai-response' || provider.id === 'aihubmix') + const showVerbositySetting = isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider) + const isSupportFlexServiceTier = isSupportFlexServiceTierModel(model) const isSupportServiceTier = isSupportServiceTierProvider(provider) - const isSupportedFlexServiceTier = isSupportFlexServiceTierModel(model) + const showServiceTierSetting = isSupportServiceTier && providerId !== SystemProviderIds.groq const setSummaryText = useCallback( (value: OpenAISummaryText) => { @@ -83,8 +82,8 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti const summaryTextOptions = [ { - value: undefined, - label: t('common.default') + value: 'undefined', + label: t('common.ignore') }, { value: 'auto', @@ -103,8 +102,8 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti const verbosityOptions = useMemo(() => { const allOptions = [ { - value: undefined, - label: t('common.default') + value: 'undefined', + label: t('common.ignore') }, { value: 'low', @@ -119,73 +118,44 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti label: t('settings.openai.verbosity.high') } ] as const satisfies VerbosityOption[] - const supportedVerbosityLevels = getModelSupportedVerbosity(model) + const supportedVerbosityLevels = getModelSupportedVerbosity(model).map((v) => toOptionValue(v)) return allOptions.filter((option) => supportedVerbosityLevels.includes(option.value)) }, [model, t]) const serviceTierOptions = useMemo(() => { - let options: ServiceTierOptions - if (provider.id === SystemProviderIds.groq) { - options = [ - { - value: null, - label: t('common.off') - }, - { - value: undefined, - label: t('common.default') - }, - { - value: 'auto', - label: t('settings.openai.service_tier.auto') - }, - { - value: 'on_demand', - label: t('settings.openai.service_tier.on_demand') - }, - { - value: 'flex', - label: t('settings.openai.service_tier.flex') - } - ] as const satisfies GroqServiceTierOption[] - } else { - // 其他情况默认是和 OpenAI 相同 - options = [ - { - 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') - }, - { - value: 'priority', - label: t('settings.openai.service_tier.priority') - } - ] as const satisfies OpenAIServiceTierOption[] - } + const options = [ + { + value: 'undefined', + label: t('common.ignore') + }, + { + value: 'null', + label: t('common.off') + }, + { + 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') + }, + { + value: 'priority', + label: t('settings.openai.service_tier.priority') + } + ] as const satisfies OpenAIServiceTierOption[] return options.filter((option) => { if (option.value === 'flex') { - return isSupportedFlexServiceTier + return isSupportFlexServiceTier } return true }) - }, [isSupportedFlexServiceTier, provider.id, t]) - - useEffect(() => { - if (serviceTierMode && !serviceTierOptions.some((option) => option.value === serviceTierMode)) { - if (provider.id === SystemProviderIds.groq) { - setServiceTierMode(GroqServiceTiers.on_demand) - } else { - setServiceTierMode(OpenAIServiceTiers.auto) - } - } - }, [provider.id, serviceTierMode, serviceTierOptions, setServiceTierMode]) + }, [isSupportFlexServiceTier, t]) useEffect(() => { if (verbosity && !verbosityOptions.some((option) => option.value === verbosity)) { @@ -196,14 +166,14 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti } }, [model, verbosity, verbosityOptions, setVerbosity]) - if (!isOpenAIReasoning && !isSupportServiceTier && !isSupportVerbosity) { + if (!showSummarySetting && !showServiceTierSetting && !showVerbositySetting) { return null } return ( - {isSupportServiceTier && ( + {showServiceTierSetting && ( <> @@ -213,18 +183,17 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti { - setServiceTierMode(value as OpenAIServiceTier) + setServiceTierMode(toRealValue(value)) }} options={serviceTierOptions} - placeholder={t('settings.openai.service_tier.auto')} /> - {(isOpenAIReasoning || isSupportVerbosity) && } + {(showSummarySetting || showVerbositySetting) && } )} - {isOpenAIReasoning && ( + {showSummarySetting && ( <> @@ -241,10 +210,10 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti options={summaryTextOptions} /> - {isSupportVerbosity && } + {showVerbositySetting && } )} - {isSupportVerbosity && ( + {showVerbositySetting && ( {t('settings.openai.verbosity.title')}{' '} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx index 8a47152a83..3e4aa4c7c7 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx @@ -76,6 +76,17 @@ const ApiOptionsSettings = ({ providerId }: Props) => { }) }, checked: !provider.apiOptions?.isNotSupportEnableThinking + }, + { + key: 'openai_verbosity', + label: t('settings.provider.api.options.verbosity.label'), + tip: t('settings.provider.api.options.verbosity.help'), + onChange: (checked: boolean) => { + updateProviderTransition({ + apiOptions: { ...provider.apiOptions, isNotSupportVerbosity: !checked } + }) + }, + checked: !provider.apiOptions?.isNotSupportVerbosity } ], [t, provider, updateProviderTransition] diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 2bb9079370..eec3b1e91c 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 177, + version: 178, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index b392091be6..d375f1de81 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2871,6 +2871,19 @@ const migrateConfig = { logger.error('migrate 177 error', error as Error) return state } + }, + '178': (state: RootState) => { + try { + const groq = state.llm.providers.find((p) => p.id === SystemProviderIds.groq) + if (groq) { + groq.verbosity = undefined + } + logger.info('migrate 178 success') + return state + } catch (error) { + logger.error('migrate 178 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index cb871d37e6..d6c6856063 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -376,7 +376,7 @@ export const initialState: SettingsState = { openAI: { summaryText: 'auto', serviceTier: 'auto', - verbosity: 'medium' + verbosity: undefined }, notification: { assistant: false, diff --git a/src/renderer/src/types/provider.ts b/src/renderer/src/types/provider.ts index 05988f6a1f..9d948f16d0 100644 --- a/src/renderer/src/types/provider.ts +++ b/src/renderer/src/types/provider.ts @@ -20,28 +20,30 @@ export const ProviderTypeSchema = z.enum([ export type ProviderType = z.infer -// undefined 视为支持,默认支持 +// undefined is treated as supported, enabled by default export type ProviderApiOptions = { - /** 是否不支持 message 的 content 为数组类型 */ + /** Whether message content of array type is not supported */ isNotSupportArrayContent?: boolean - /** 是否不支持 stream_options 参数 */ + /** Whether the stream_options parameter is not supported */ isNotSupportStreamOptions?: boolean /** * @deprecated - * 是否不支持 message 的 role 为 developer */ + * Whether message role 'developer' is not supported */ isNotSupportDeveloperRole?: boolean - /* 是否支持 message 的 role 为 developer */ + /* Whether message role 'developer' is supported */ isSupportDeveloperRole?: boolean /** * @deprecated - * 是否不支持 service_tier 参数. Only for OpenAI Models. */ + * Whether the service_tier parameter is not supported. Only for OpenAI Models. */ isNotSupportServiceTier?: boolean - /* 是否支持 service_tier 参数. Only for OpenAI Models. */ + /* Whether the service_tier parameter is supported. Only for OpenAI Models. */ isSupportServiceTier?: boolean - /** 是否不支持 enable_thinking 参数 */ + /** Whether the enable_thinking parameter is not supported */ isNotSupportEnableThinking?: boolean - /** 是否不支持 APIVersion */ + /** Whether APIVersion is not supported */ isNotSupportAPIVersion?: boolean + /** Whether verbosity is not supported. For OpenAI API (completions & responses). */ + isNotSupportVerbosity?: boolean } // scale is not well supported now. It even lacks of docs @@ -61,6 +63,7 @@ export function isOpenAIServiceTier(tier: string | null | undefined): tier is Op } // https://console.groq.com/docs/api-reference#responses +// null is not used. export type GroqServiceTier = 'auto' | 'on_demand' | 'flex' | undefined | null export const GroqServiceTiers = { diff --git a/src/renderer/src/utils/__tests__/provider.test.ts b/src/renderer/src/utils/__tests__/provider.test.ts index eef97ce67e..a7823eda06 100644 --- a/src/renderer/src/utils/__tests__/provider.test.ts +++ b/src/renderer/src/utils/__tests__/provider.test.ts @@ -19,7 +19,8 @@ import { isSupportEnableThinkingProvider, isSupportServiceTierProvider, isSupportStreamOptionsProvider, - isSupportUrlContextProvider + isSupportUrlContextProvider, + isSupportVerbosityProvider } from '../provider' vi.mock('@renderer/store/settings', () => ({ @@ -104,6 +105,35 @@ describe('provider utils', () => { expect(isSupportServiceTierProvider(createSystemProvider({ id: SystemProviderIds.github }))).toBe(false) }) + it('determines verbosity support', () => { + // Custom providers with explicit flag + expect(isSupportVerbosityProvider(createProvider({ apiOptions: { isNotSupportVerbosity: false } }))).toBe(true) + expect(isSupportVerbosityProvider(createProvider({ apiOptions: { isNotSupportVerbosity: true } }))).toBe(false) + + // Custom providers without apiOptions (should support by default) + expect(isSupportVerbosityProvider(createProvider())).toBe(true) + expect(isSupportVerbosityProvider(createProvider({ apiOptions: {} }))).toBe(true) + + // System providers that support verbosity (default behavior) + expect(isSupportVerbosityProvider(createSystemProvider())).toBe(true) + expect(isSupportVerbosityProvider(createSystemProvider({ id: SystemProviderIds.openai }))).toBe(true) + + // System providers in the NOT_SUPPORT_VERBOSITY_PROVIDERS list (cannot be overridden by apiOptions) + expect(isSupportVerbosityProvider(createSystemProvider({ id: SystemProviderIds.groq }))).toBe(false) + expect( + isSupportVerbosityProvider( + createSystemProvider({ id: SystemProviderIds.groq, apiOptions: { isNotSupportVerbosity: false } }) + ) + ).toBe(false) + + // apiOptions can disable verbosity for any provider + expect( + isSupportVerbosityProvider( + createSystemProvider({ id: SystemProviderIds.openai, apiOptions: { isNotSupportVerbosity: true } }) + ) + ).toBe(false) + }) + it('detects URL context capable providers', () => { expect(isSupportUrlContextProvider(createProvider({ type: 'gemini' }))).toBe(true) expect( diff --git a/src/renderer/src/utils/provider.ts b/src/renderer/src/utils/provider.ts index 7ee9e0bf6d..e8fc1b5cc7 100644 --- a/src/renderer/src/utils/provider.ts +++ b/src/renderer/src/utils/provider.ts @@ -83,6 +83,21 @@ export const isSupportServiceTierProvider = (provider: Provider) => { ) } +const NOT_SUPPORT_VERBOSITY_PROVIDERS = ['groq'] as const satisfies SystemProviderId[] + +/** + * Determines whether the provider supports the verbosity option. + * Only applies to system providers that are not in the exclusion list. + * @param provider - The provider to check + * @returns true if the provider supports verbosity, false otherwise + */ +export const isSupportVerbosityProvider = (provider: Provider) => { + return ( + provider.apiOptions?.isNotSupportVerbosity !== true && + !NOT_SUPPORT_VERBOSITY_PROVIDERS.some((pid) => pid === provider.id) + ) +} + const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [ 'gemini', 'vertexai', diff --git a/src/renderer/src/utils/select.ts b/src/renderer/src/utils/select.ts new file mode 100644 index 0000000000..cf1eaa19d8 --- /dev/null +++ b/src/renderer/src/utils/select.ts @@ -0,0 +1,36 @@ +/** + * Convert a value (string | undefined | null) into an option-compatible string. + * - `undefined` becomes the literal string `'undefined'` + * - `null` becomes the literal string `'null'` + * - Any other string is returned as-is + * + * @param v - The value to convert + * @returns The string representation safe for option usage + */ +export function toOptionValue>(v: T): NonNullable | 'undefined' +export function toOptionValue>(v: T): NonNullable | 'null' +export function toOptionValue(v: T): NonNullable | 'undefined' | 'null' +export function toOptionValue>(v: T): T +export function toOptionValue(v: string | undefined | null) { + if (v === undefined) return 'undefined' + if (v === null) return 'null' + return v +} + +/** + * Convert an option string back to its original value. + * - The literal string `'undefined'` becomes `undefined` + * - The literal string `'null'` becomes `null` + * - Any other string is returned as-is + * + * @param v - The option string to convert + * @returns The real value (`undefined`, `null`, or the original string) + */ +export function toRealValue(v: T): undefined +export function toRealValue(v: T): null +export function toRealValue(v: T): Exclude +export function toRealValue(v: string) { + if (v === 'undefined') return undefined + if (v === 'null') return null + return v +}