feat: enable thinking control (#8946)

* feat(types): 添加AtLeast泛型类型用于定义至少包含指定键的对象

新增AtLeast泛型类型,用于表示一个对象至少包含类型T中指定的所有键(值类型为U),同时允许包含其他任意string类型的键(值类型也必须是U)。该类型在providers.ts中被用于定义PROVIDER_LOGO_MAP的类型约束,并调整了NOT_SUPPORTED_REANK_PROVIDERS和ONLY_SUPPORTED_DIMENSION_PROVIDERS的类型定义。

* feat(provider): 添加 enable_thinking 支持并重构 API 选项配置

将分散的 provider API 选项配置重构为统一的 ProviderApiOptions 类型
添加对 enable_thinking 参数的支持
实现配置迁移逻辑将旧配置转换为新格式

* fix(providers): 修正provider权限检查逻辑

将权限检查从直接访问provider属性改为通过apiOptions访问,确保一致性

* refactor(providers): 重命名并扩展 enable_thinking 参数支持检查

将 isSupportQwen3EnableThinkingProvider 重命名为 isSupportEnableThinkingProvider 并扩展其功能
现在支持通过 apiOptions.isNotSupportEnableThinking 配置来禁用 enable_thinking 参数
同时保持对 Qwen3 等模型的原有支持逻辑

* refactor(providers): 修改 isSupportEnableThinkingProvider 为黑名单逻辑

* fix(providers): 修复数组内容支持判断逻辑

检查 provider.apiOptions?.isNotSupportArrayContent 而非直接检查 provider.isNotSupportArrayContent

* docs(providers): 添加关于NOT_SUPPORT_QWEN3_ENABLE_THINKING_PROVIDER的注释

* fix(store): 更新持久化存储版本号至129

* feat(i18n): 为多语言文件添加 enable_thinking 参数支持

* refactor(ProviderSettings): 移除重复的provider展开操作符

更新ApiOptionsSettings组件,移除updateProviderTransition中不必要的provider展开操作符

* test(utils): 修复测试文件中provider的类型定义

将provider对象明确标记为const并添加satisfies Provider类型约束,确保类型安全

* fix(providers): 修正NOT_SUPPORTED_REANK_PROVIDERS变量名拼写错误并添加lmstudio

修复NOT_SUPPORTED_REANK_PROVIDERS变量名拼写错误为NOT_SUPPORTED_RERANK_PROVIDERS,并添加lmstudio到不支持重排的提供商列表
This commit is contained in:
Phantom 2025-08-09 00:05:40 +08:00 committed by GitHub
parent 73dc3325df
commit 5647d6e6d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 162 additions and 40 deletions

View File

@ -27,7 +27,7 @@ import {
import {
isSupportArrayContentProvider,
isSupportDeveloperRoleProvider,
isSupportQwen3EnableThinkingProvider,
isSupportEnableThinkingProvider,
isSupportStreamOptionsProvider
} from '@renderer/config/providers'
import { processPostsuffixQwen3Model, processReqMessages } from '@renderer/services/ModelMessageService'
@ -151,7 +151,10 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return { reasoning: { enabled: false, exclude: true } }
}
if (isSupportedThinkingTokenQwenModel(model) || isSupportedThinkingTokenHunyuanModel(model)) {
if (
isSupportEnableThinkingProvider(this.provider) &&
(isSupportedThinkingTokenQwenModel(model) || isSupportedThinkingTokenHunyuanModel(model))
) {
return { enable_thinking: false }
}
@ -201,7 +204,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
// Qwen models
if (isQwenReasoningModel(model)) {
const thinkConfig = {
enable_thinking: isQwenAlwaysThinkModel(model) ? undefined : true,
enable_thinking:
isQwenAlwaysThinkModel(model) || !isSupportEnableThinkingProvider(this.provider) ? undefined : true,
thinking_budget: budgetTokens
}
if (this.provider.id === 'dashscope') {
@ -214,7 +218,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
}
// Hunyuan models
if (isSupportedThinkingTokenHunyuanModel(model)) {
if (isSupportedThinkingTokenHunyuanModel(model) && isSupportEnableThinkingProvider(this.provider)) {
return {
enable_thinking: true
}
@ -544,7 +548,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
if (
lastUserMsg &&
isSupportedThinkingTokenQwenModel(model) &&
!isSupportQwen3EnableThinkingProvider(this.provider)
!isSupportEnableThinkingProvider(this.provider)
) {
const postsuffix = '/no_think'
const qwenThinkModeEnabled = assistant.settings?.qwenThinkMode === true

View File

@ -52,7 +52,7 @@ import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { OpenAIServiceTiers, Provider, SystemProvider, SystemProviderId } from '@renderer/types'
import { AtLeast, OpenAIServiceTiers, Provider, SystemProvider, SystemProviderId } from '@renderer/types'
import { TOKENFLUX_HOST } from './constant'
import { SYSTEM_MODELS } from './models'
@ -593,7 +593,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
const PROVIDER_LOGO_MAP = {
const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
ph8: Ph8ProviderLogo,
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo,
@ -656,8 +656,8 @@ export function getProviderLogo(providerId: string) {
}
// export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina', 'voyageai', 'dashscope', 'aihubmix']
export const NOT_SUPPORTED_REANK_PROVIDERS = ['ollama']
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
export const NOT_SUPPORTED_RERANK_PROVIDERS = ['ollama', 'lmstudio'] as const satisfies SystemProviderId[]
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini'] as const satisfies SystemProviderId[]
type ProviderUrls = {
api: {
@ -1247,7 +1247,7 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
*/
export const isSupportArrayContentProvider = (provider: Provider) => {
return (
provider.isNotSupportArrayContent !== true &&
provider.apiOptions?.isNotSupportArrayContent !== true &&
!NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS.some((pid) => pid === provider.id)
)
}
@ -1259,7 +1259,7 @@ const NOT_SUPPORT_DEVELOPER_ROLE_PROVIDERS = ['poe'] as const satisfies SystemPr
*/
export const isSupportDeveloperRoleProvider = (provider: Provider) => {
return (
provider.isNotSupportDeveloperRole !== true &&
provider.apiOptions?.isNotSupportDeveloperRole !== true &&
!NOT_SUPPORT_DEVELOPER_ROLE_PROVIDERS.some((pid) => pid === provider.id)
)
}
@ -1271,18 +1271,22 @@ const NOT_SUPPORT_STREAM_OPTIONS_PROVIDERS = ['mistral'] as const satisfies Syst
*/
export const isSupportStreamOptionsProvider = (provider: Provider) => {
return (
provider.isNotSupportStreamOptions !== true &&
provider.apiOptions?.isNotSupportStreamOptions !== true &&
!NOT_SUPPORT_STREAM_OPTIONS_PROVIDERS.some((pid) => pid === provider.id)
)
}
const SUPPORT_QWEN3_ENABLE_THINKING_PROVIDER = ['dashscope', 'modelscope'] as const satisfies SystemProviderId[]
// NOTE: 暂时不知道哪些系统提供商不支持该参数,先默认都支持。出问题的时候可以先用自定义参数顶着
const NOT_SUPPORT_QWEN3_ENABLE_THINKING_PROVIDER = [] as const satisfies SystemProviderId[]
/**
* 使enable_thinking参数来控制Qwen3系列模型的思 Only for OpenAI Chat Completions API.
* 使 enable_thinking Qwen3 Only for OpenAI Chat Completions API.
*/
export const isSupportQwen3EnableThinkingProvider = (provider: Provider) => {
return SUPPORT_QWEN3_ENABLE_THINKING_PROVIDER.some((pid) => pid === provider.id)
export const isSupportEnableThinkingProvider = (provider: Provider) => {
return (
provider.apiOptions?.isNotSupportEnableThinking !== true &&
!NOT_SUPPORT_QWEN3_ENABLE_THINKING_PROVIDER.some((pid) => pid === provider.id)
)
}
const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
@ -1292,6 +1296,7 @@ const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot'] as const satisf
*/
export const isSupportServiceTierProviders = (provider: Provider) => {
return (
provider.isNotSupportServiceTier !== true || !NOT_SUPPORT_SERVICE_TIER_PROVIDERS.some((pid) => pid === provider.id)
provider.apiOptions?.isNotSupportServiceTier !== true &&
!NOT_SUPPORT_SERVICE_TIER_PROVIDERS.some((pid) => pid === provider.id)
)
}

View File

@ -3160,6 +3160,10 @@
"help": "Does the provider support messages with role: \"developer\"?",
"label": "Support Developer Message"
},
"enable_thinking": {
"help": "Does the provider support controlling the reasoning of models like Qwen3 via the enable_thinking parameter?",
"label": "Support enable_thinking"
},
"label": "API Settings",
"service_tier": {
"help": "Whether the provider supports configuring the service_tier parameter. When enabled, this parameter can be adjusted in the service tier settings on the chat page. (OpenAI models only)",

View File

@ -3160,6 +3160,10 @@
"help": "このプロバイダーは role: \"developer\" のメッセージをサポートしていますか",
"label": "Developer Message をサポート"
},
"enable_thinking": {
"help": "このプロバイダーは、enable_thinking パラメータを使用して Qwen3 などのモデルの思考を制御することをサポートしていますか。",
"label": "enable_thinking をサポート"
},
"label": "API設定",
"service_tier": {
"help": "このプロバイダーがservice_tierパラメータの設定をサポートしているかどうか。有効にすると、チャットページのサービスレベル設定でこのパラメータを調整できます。OpenAIモデルのみ対象",

View File

@ -3160,6 +3160,10 @@
"help": "Предоставляет ли этот провайдер сообщения с ролью: \"разработчик\"",
"label": "Поддержка сообщения разработчика"
},
"enable_thinking": {
"help": "Поддерживает ли данный провайдер возможность управления мышлением моделей, таких как Qwen3, с помощью параметра enable_thinking",
"label": "Поддержка enable_thinking"
},
"label": "API настройки",
"service_tier": {
"help": "Поддерживает ли этот провайдер настройку параметра service_tier? После включения параметр можно настроить в настройках уровня обслуживания на странице диалога. (Только для моделей OpenAI)",

View File

@ -3160,6 +3160,10 @@
"help": "该提供商是否支持 role: \"developer\" 的消息",
"label": "支持 Developer Message"
},
"enable_thinking": {
"help": "该提供商是否支持通过 enable_thinking 参数控制 Qwen3 等模型的思考",
"label": "支持 enable_thinking"
},
"label": "API 设置",
"service_tier": {
"help": "该提供商是否支持配置 service_tier 参数。开启后可在对话页面的服务层级设置中调整该参数。仅限OpenAI模型",

View File

@ -3160,6 +3160,10 @@
"help": "該提供商是否支援 role: \"developer\" 的訊息",
"label": "支援開發人員訊息"
},
"enable_thinking": {
"help": "該提供商是否支援透過 enable_thinking 參數控制 Qwen3 等模型的思考",
"label": "支援 enable_thinking"
},
"label": "API 設定",
"service_tier": {
"help": "該提供商是否支援設定 service_tier 參數。啟用後,可在對話頁面的服務層級設定中調整此參數。(僅限 OpenAI 模型)",

View File

@ -3160,6 +3160,10 @@
"help": "Ο πάροχος υποστηρίζει μηνύματα με ρόλο: \"developer\";",
"label": "Υποστήριξη μηνύματος προγραμματιστή"
},
"enable_thinking": {
"help": "Ο πάροχος υποστηρίζει τον έλεγχο της σκέψης μοντέλων όπως το Qwen3 μέσω της παραμέτρου enable_thinking;",
"label": "Υποστήριξη enable_thinking"
},
"label": "Ρυθμίσεις API",
"service_tier": {
"help": "Εάν ο πάροχος υποστηρίζει τη δυνατότητα διαμόρφωσης της παραμέτρου service_tier. Αν είναι ενεργοποιημένη, αυτή η παράμετρος μπορεί να ρυθμιστεί μέσω της ρύθμισης επιπέδου υπηρεσίας στη σελίδα διαλόγου. (Μόνο για μοντέλα OpenAI)",

View File

@ -3160,6 +3160,10 @@
"help": "¿Admite el proveedor mensajes con el rol: \"developer\"?",
"label": "Mensajes para desarrolladores compatibles"
},
"enable_thinking": {
"help": "¿Admite este proveedor el control del pensamiento de modelos como Qwen3 mediante el parámetro enable_thinking?",
"label": "Soporta enable_thinking"
},
"label": "Configuración de la API",
"service_tier": {
"help": "Si el proveedor admite la configuración del parámetro service_tier. Al activarlo, se podrá ajustar este parámetro en la configuración del nivel de servicio en la página de conversación. (Solo para modelos OpenAI)",

View File

@ -3160,6 +3160,10 @@
"help": "Le fournisseur prend-il en charge les messages avec le rôle : « développeur » ?",
"label": "Prise en charge du message développeur"
},
"enable_thinking": {
"help": "Le fournisseur prend-il en charge le contrôle de la réflexion des modèles tels que Qwen3 via le paramètre enable_thinking ?",
"label": "Prise en charge de enable_thinking"
},
"label": "Paramètres de l'API",
"service_tier": {
"help": "Le fournisseur prend-il en charge la configuration du paramètre service_tier ? Lorsqu'il est activé, ce paramètre peut être ajusté dans les paramètres de niveau de service sur la page de conversation. (Modèles OpenAI uniquement)",

View File

@ -3160,6 +3160,10 @@
"help": "O fornecedor suporta mensagens com role: \"developer\"?",
"label": "Mensagem de suporte ao programador"
},
"enable_thinking": {
"help": "O fornecedor suporta o controlo do pensamento de modelos como o Qwen3 através do parâmetro enable_thinking?",
"label": "Apoiar enable_thinking"
},
"label": "Definições da API",
"service_tier": {
"help": "Se o fornecedor suporta a configuração do parâmetro service_tier. Quando ativado, este parâmetro pode ser ajustado nas definições do nível de serviço na página de conversa. (Apenas para modelos OpenAI)",

View File

@ -38,36 +38,55 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
label: t('settings.provider.api.options.developer_role.label'),
tip: t('settings.provider.api.options.developer_role.help'),
onChange: (checked: boolean) => {
updateProviderTransition({ ...provider, isNotSupportDeveloperRole: !checked })
updateProviderTransition({
apiOptions: { ...provider.apiOptions, isNotSupportDeveloperRole: !checked }
})
},
checked: !provider.isNotSupportDeveloperRole
checked: !provider.apiOptions?.isNotSupportDeveloperRole
},
{
key: 'openai_stream_options',
label: t('settings.provider.api.options.stream_options.label'),
tip: t('settings.provider.api.options.stream_options.help'),
onChange: (checked: boolean) => {
updateProviderTransition({ ...provider, isNotSupportStreamOptions: !checked })
updateProviderTransition({
apiOptions: { ...provider.apiOptions, isNotSupportStreamOptions: !checked }
})
},
checked: !provider.isNotSupportStreamOptions
checked: !provider.apiOptions?.isNotSupportStreamOptions
},
{
key: 'openai_array_content',
label: t('settings.provider.api.options.array_content.label'),
tip: t('settings.provider.api.options.array_content.help'),
onChange: (checked: boolean) => {
updateProviderTransition({ ...provider, isNotSupportArrayContent: !checked })
updateProviderTransition({
apiOptions: { ...provider.apiOptions, isNotSupportArrayContent: !checked }
})
},
checked: !provider.isNotSupportArrayContent
checked: !provider.apiOptions?.isNotSupportArrayContent
},
{
key: 'openai_service_tier',
label: t('settings.provider.api.options.service_tier.label'),
tip: t('settings.provider.api.options.service_tier.help'),
onChange: (checked: boolean) => {
updateProviderTransition({ ...provider, isNotSupportServiceTier: !checked })
updateProviderTransition({
apiOptions: { ...provider.apiOptions, isNotSupportServiceTier: !checked }
})
},
checked: !provider.isNotSupportServiceTier
checked: !provider.apiOptions?.isNotSupportServiceTier
},
{
key: 'openai_enable_thinking',
label: t('settings.provider.api.options.enable_thinking.label'),
tip: t('settings.provider.api.options.enable_thinking.help'),
onChange: (checked: boolean) => {
updateProviderTransition({
apiOptions: { ...provider.apiOptions, isNotSupportEnableThinking: !checked }
})
},
checked: !provider.apiOptions?.isNotSupportEnableThinking
}
],
[t, provider, updateProviderTransition]

View File

@ -2,7 +2,7 @@ import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimensio
import ModelSelector from '@renderer/components/ModelSelector'
import { DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT } from '@renderer/config/constant'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { NOT_SUPPORTED_REANK_PROVIDERS } from '@renderer/config/providers'
import { NOT_SUPPORTED_RERANK_PROVIDERS } from '@renderer/config/providers'
import { useProviders } from '@renderer/hooks/useProvider'
import { useWebSearchSettings } from '@renderer/hooks/useWebSearchProviders'
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
@ -30,7 +30,7 @@ const RagSettings = () => {
}, [providers])
const rerankProviders = useMemo(() => {
return providers.filter((p) => !NOT_SUPPORTED_REANK_PROVIDERS.includes(p.id))
return providers.filter((p) => !NOT_SUPPORTED_RERANK_PROVIDERS.some((pid) => p.id === pid))
}, [providers])
const handleEmbeddingModelChange = (modelValue: string) => {

View File

@ -60,7 +60,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 128,
version: 129,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@ -18,6 +18,7 @@ import {
LanguageCode,
Model,
Provider,
ProviderApiOptions,
SystemProviderIds,
WebSearchProvider
} from '@renderer/types'
@ -2054,6 +2055,29 @@ const migrateConfig = {
logger.error('migrate 128 error', error as Error)
return state
}
},
'129': (state: RootState) => {
try {
// 聚合 api options
state.llm.providers.forEach((p) => {
if (isSystemProvider(p)) {
updateProvider(state, p.id, { apiOptions: undefined })
} else {
const changes: ProviderApiOptions = {
isNotSupportArrayContent: p.isNotSupportArrayContent,
isNotSupportServiceTier: p.isNotSupportServiceTier,
isNotSupportDeveloperRole: p.isNotSupportDeveloperRole,
isNotSupportStreamOptions: p.isNotSupportStreamOptions
}
updateProvider(state, p.id, { apiOptions: changes })
}
})
return state
} catch (error) {
logger.error('migrate 129 error', error as Error)
return state
}
}
}

View File

@ -192,6 +192,20 @@ export type User = {
email: string
}
// undefined 视为支持,默认支持
export type ProviderApiOptions = {
/** 是否不支持 message 的 content 为数组类型 */
isNotSupportArrayContent?: boolean
/** 是否不支持 stream_options 参数 */
isNotSupportStreamOptions?: boolean
/** 是否不支持 message 的 role 为 developer */
isNotSupportDeveloperRole?: boolean
/** 是否不支持 service_tier 参数. Only for OpenAI Models. */
isNotSupportServiceTier?: boolean
/** 是否不支持 enable_thinking 参数 */
isNotSupportEnableThinking?: boolean
}
export type Provider = {
id: string
type: ProviderType
@ -206,17 +220,18 @@ export type Provider = {
rateLimit?: number
// API options
// undefined 视为支持,默认支持
/** 是否不支持 message 的 content 为数组类型 */
isNotSupportArrayContent?: boolean
/** 是否不支持 stream_options 参数 */
isNotSupportStreamOptions?: boolean
/** 是否不支持 message 的 role 为 developer */
isNotSupportDeveloperRole?: boolean
/** 是否不支持 service_tier 参数. Only for OpenAI Models. */
isNotSupportServiceTier?: boolean
apiOptions?: ProviderApiOptions
serviceTier?: ServiceTier
/** @deprecated */
isNotSupportArrayContent?: boolean
/** @deprecated */
isNotSupportStreamOptions?: boolean
/** @deprecated */
isNotSupportDeveloperRole?: boolean
/** @deprecated */
isNotSupportServiceTier?: boolean
isVertex?: boolean
notes?: string
extra_headers?: Record<string, string>
@ -286,6 +301,7 @@ export const isSystemProviderId = (id: string): id is SystemProviderId => {
export type SystemProvider = Provider & {
id: SystemProviderId
isSystem: true
apiOptions?: never
}
/**
@ -1065,3 +1081,20 @@ export interface MemoryListOptions extends MemoryEntity {
export interface MemoryDeleteAllOptions extends MemoryEntity {}
// ========================================================================
/**
* T中指定的所有键U
* string类型的键U
* @template T -
* @template U -
* @example
* type Example = AtLeast<'a' | 'b', number>;
* // 结果类型允许:
* const obj1: Example = { a: 1, b: 2 }; // 只包含必需的键
* const obj2: Example = { a: 1, b: 2, c: 3 }; // 包含额外的键
*/
export type AtLeast<T extends string, U> = {
[K in T]: U
} & {
[key: string]: U
}

View File

@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
import { includeKeywords, matchKeywordsInModel, matchKeywordsInProvider, matchKeywordsInString } from '../match'
describe('match', () => {
const provider: Provider = {
const provider = {
id: '12345',
type: 'openai',
name: 'OpenAI',
@ -12,13 +12,14 @@ describe('match', () => {
apiHost: '',
models: [],
isSystem: false
}
} as const satisfies Provider
const sysProvider: SystemProvider = {
...provider,
id: 'dashscope',
name: 'doesnt matter',
isSystem: true
}
} as const
describe('includeKeywords', () => {
it('should return true if keywords is empty or blank', () => {