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
This commit is contained in:
Phantom 2025-11-25 23:29:03 +08:00 committed by GitHub
parent 69d31a1e2b
commit e8de31ca64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 352 additions and 104 deletions

View File

@ -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

View File

@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
interface SelectorOption<V = string | number | undefined | null> {
interface SelectorOption<V = string | number> {
label: string | ReactNode
value: V
type?: 'group'
@ -14,7 +14,7 @@ interface SelectorOption<V = string | number | undefined | null> {
disabled?: boolean
}
interface BaseSelectorProps<V = string | number | undefined | null> {
interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[]
placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
@ -39,7 +39,7 @@ interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
export type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
const Selector = <V extends string | number | undefined | null>({
const Selector = <V extends string | number>({
options,
value,
onChange = () => {},

View File

@ -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)
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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> = (props) => {
const model = assistant.model || getDefaultModel()
const isOpenAI = isOpenAIModel(model)
const showOpenAiSettings = isOpenAIModel(model) || isSupportServiceTierProvider(provider)
return (
<Container className="settings-tab">
@ -332,7 +334,7 @@ const SettingsTab: FC<Props> = (props) => {
</SettingGroup>
</CollapsibleSettingGroup>
)}
{isOpenAI && (
{showOpenAiSettings && (
<OpenAISettingsGroup
model={model}
providerId={provider.id}
@ -340,6 +342,9 @@ const SettingsTab: FC<Props> = (props) => {
SettingRowTitleSmall={SettingRowTitleSmall}
/>
)}
{isGroqSystemProvider(provider) && (
<GroqSettingsGroup SettingGroup={SettingGroup} SettingRowTitleSmall={SettingRowTitleSmall} />
)}
<CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>

View File

@ -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<GroqServiceTier> | 'undefined'; label: string }
interface Props {
SettingGroup: FC<{ children: React.ReactNode }>
SettingRowTitleSmall: FC<{ children: React.ReactNode }>
}
const GroqSettingsGroup: FC<Props> = ({ 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 (
<CollapsibleSettingGroup title={t('settings.groq.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>
{t('settings.openai.service_tier.title')}{' '}
<Tooltip title={t('settings.openai.service_tier.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Selector
value={toOptionValue(serviceTierMode)}
onChange={(value) => {
setServiceTierMode(toRealValue(value))
}}
options={serviceTierOptions}
/>
</SettingRow>
</SettingGroup>
<SettingDivider />
</CollapsibleSettingGroup>
)
}
export default GroqSettingsGroup

View File

@ -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<OpenAIVerbosity> | 'undefined'
label: string
}
type SummaryTextOption = {
value: OpenAISummaryText
value: NonNullable<OpenAISummaryText> | 'undefined'
label: string
}
type OpenAIServiceTierOption = { value: OpenAIServiceTier; label: string }
type GroqServiceTierOption = { value: GroqServiceTier; label: string }
type ServiceTierOptions = OpenAIServiceTierOption[] | GroqServiceTierOption[]
type OpenAIServiceTierOption = { value: NonNullable<OpenAIServiceTier> | 'null' | 'undefined'; label: string }
interface Props {
model: Model
@ -52,13 +50,14 @@ const OpenAISettingsGroup: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ model, providerId, SettingGroup, Setti
}
}, [model, verbosity, verbosityOptions, setVerbosity])
if (!isOpenAIReasoning && !isSupportServiceTier && !isSupportVerbosity) {
if (!showSummarySetting && !showServiceTierSetting && !showVerbositySetting) {
return null
}
return (
<CollapsibleSettingGroup title={t('settings.openai.title')} defaultExpanded={true}>
<SettingGroup>
{isSupportServiceTier && (
{showServiceTierSetting && (
<>
<SettingRow>
<SettingRowTitleSmall>
@ -213,18 +183,17 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
</Tooltip>
</SettingRowTitleSmall>
<Selector
value={serviceTierMode}
value={toOptionValue(serviceTierMode)}
onChange={(value) => {
setServiceTierMode(value as OpenAIServiceTier)
setServiceTierMode(toRealValue(value))
}}
options={serviceTierOptions}
placeholder={t('settings.openai.service_tier.auto')}
/>
</SettingRow>
{(isOpenAIReasoning || isSupportVerbosity) && <SettingDivider />}
{(showSummarySetting || showVerbositySetting) && <SettingDivider />}
</>
)}
{isOpenAIReasoning && (
{showSummarySetting && (
<>
<SettingRow>
<SettingRowTitleSmall>
@ -241,10 +210,10 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
options={summaryTextOptions}
/>
</SettingRow>
{isSupportVerbosity && <SettingDivider />}
{showVerbositySetting && <SettingDivider />}
</>
)}
{isSupportVerbosity && (
{showVerbositySetting && (
<SettingRow>
<SettingRowTitleSmall>
{t('settings.openai.verbosity.title')}{' '}

View File

@ -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]

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 177,
version: 178,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate
},

View File

@ -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
}
}
}

View File

@ -376,7 +376,7 @@ export const initialState: SettingsState = {
openAI: {
summaryText: 'auto',
serviceTier: 'auto',
verbosity: 'medium'
verbosity: undefined
},
notification: {
assistant: false,

View File

@ -20,28 +20,30 @@ export const ProviderTypeSchema = z.enum([
export type ProviderType = z.infer<typeof ProviderTypeSchema>
// 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 = {

View File

@ -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(

View File

@ -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',

View File

@ -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<T extends undefined | Exclude<string, null>>(v: T): NonNullable<T> | 'undefined'
export function toOptionValue<T extends null | Exclude<string, undefined>>(v: T): NonNullable<T> | 'null'
export function toOptionValue<T extends string | undefined | null>(v: T): NonNullable<T> | 'undefined' | 'null'
export function toOptionValue<T extends Exclude<string, null | undefined>>(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<T extends 'undefined'>(v: T): undefined
export function toRealValue<T extends 'null'>(v: T): null
export function toRealValue<T extends string>(v: T): Exclude<T, 'undefined' | 'null'>
export function toRealValue(v: string) {
if (v === 'undefined') return undefined
if (v === 'null') return null
return v
}