From e8de31ca641b5ae9cbbdee52d97b72e608d4ef97 Mon Sep 17 00:00:00 2001 From: Phantom Date: Tue, 25 Nov 2025 23:29:03 +0800 Subject: [PATCH] fix: Groq verbosity setting (#11452) * feat(settings): show OpenAI settings for supported service tier providers Add support for displaying OpenAI settings when provider supports service tiers. This includes refactoring the condition check and fixing variable naming consistency. * fix(settings): set openAI verbosity to undefined by default * fix(store): bump version to 178 and disable verbosity for groq provider Add migration to remove verbosity from groq provider and implement provider utility to check verbosity support Update provider types to include verbosity support flag * feat(provider): add verbosity option support for providers Add verbosity parameter support in provider API options settings * fix(aiCore): check provider support for verbosity before applying Add provider validation and check for verbosity support to prevent errors when unsupported providers are used with verbosity settings * feat(settings): add Groq settings group component and translations add new GroqSettingsGroup component for managing Groq provider settings update translations for Groq settings in both zh-cn and en-us locales refactor OpenAISettingsGroup to separate Groq-specific logic * feat(i18n): add groq settings and verbosity support translations add translations for groq settings title and verbosity parameter support in multiple languages * refactor(settings): simplify service tier mode fallback logic Remove conditional service tier mode fallback and use provider-specific defaults directly * fix(provider): remove redundant system provider check in verbosity support * test(provider): add tests for verbosity support detection * fix(OpenAISettingsGroup): add endpoint_type check for showSummarySetting condition Add model.endpoint_type check to properly determine when to show summary setting for OpenAI models * refactor(selector): simplify selector option types and add utility functions remove undefined and null from selector option types add utility functions to convert between option values and real values update groq and openai settings groups to use new utilities add new translation for "ignore" option * fix(ApiOptionsSettings): correct checked state for verbosity toggle * feat(i18n): add "ignore" translation for multiple languages * refactor(groq): remove unused model prop and related checks Clean up GroqSettingsGroup component by removing unused model prop and unnecessary service tier checks --- src/renderer/src/aiCore/utils/options.ts | 10 +- src/renderer/src/components/Selector.tsx | 6 +- src/renderer/src/config/models/utils.ts | 1 + src/renderer/src/i18n/locales/en-us.json | 8 + src/renderer/src/i18n/locales/zh-cn.json | 8 + src/renderer/src/i18n/locales/zh-tw.json | 8 + src/renderer/src/i18n/translate/de-de.json | 8 + src/renderer/src/i18n/translate/el-gr.json | 8 + src/renderer/src/i18n/translate/es-es.json | 8 + src/renderer/src/i18n/translate/fr-fr.json | 8 + src/renderer/src/i18n/translate/ja-jp.json | 8 + src/renderer/src/i18n/translate/pt-pt.json | 8 + src/renderer/src/i18n/translate/ru-ru.json | 8 + .../src/pages/home/Tabs/SettingsTab.tsx | 11 +- .../Tabs/components/GroqSettingsGroup.tsx | 79 ++++++++++ .../Tabs/components/OpenAISettingsGroup.tsx | 137 +++++++----------- .../ApiOptionsSettings/ApiOptionsSettings.tsx | 11 ++ src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 13 ++ src/renderer/src/store/settings.ts | 2 +- src/renderer/src/types/provider.ts | 21 +-- .../src/utils/__tests__/provider.test.ts | 32 +++- src/renderer/src/utils/provider.ts | 15 ++ src/renderer/src/utils/select.ts | 36 +++++ 24 files changed, 352 insertions(+), 104 deletions(-) create mode 100644 src/renderer/src/pages/home/Tabs/components/GroqSettingsGroup.tsx create mode 100644 src/renderer/src/utils/select.ts 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 +}