diff --git a/package.json b/package.json index 2f8c0c920d..ea14d7d5f5 100644 --- a/package.json +++ b/package.json @@ -108,8 +108,10 @@ "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", "@ai-sdk/amazon-bedrock": "^3.0.53", + "@ai-sdk/anthropic": "^2.0.44", "@ai-sdk/cerebras": "^1.0.31", "@ai-sdk/gateway": "^2.0.9", + "@ai-sdk/google": "^2.0.32", "@ai-sdk/google-vertex": "^3.0.62", "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch", "@ai-sdk/mistral": "^2.0.23", diff --git a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts index 767cad1294..f520162496 100644 --- a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { + getModelSupportedVerbosity, isFunctionCallingModel, isNotSupportTemperatureAndTopP, isOpenAIModel, @@ -242,12 +243,18 @@ export abstract class BaseApiClient< return serviceTierSetting } - protected getVerbosity(): OpenAIVerbosity { + protected getVerbosity(model?: Model): OpenAIVerbosity { try { const state = window.store?.getState() const verbosity = state?.settings?.openAI?.verbosity if (verbosity && ['low', 'medium', 'high'].includes(verbosity)) { + // If model is provided, check if the verbosity is supported by the model + if (model) { + const supportedVerbosity = getModelSupportedVerbosity(model) + // Use user's verbosity if supported, otherwise use the first supported option + return supportedVerbosity.includes(verbosity) ? verbosity : supportedVerbosity[0] + } return verbosity } } catch (error) { diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts index 8ff25e356d..ad87331855 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts @@ -35,6 +35,7 @@ import { isSupportedThinkingTokenModel, isSupportedThinkingTokenQwenModel, isSupportedThinkingTokenZhipuModel, + isSupportVerbosityModel, isVisionModel, MODEL_SUPPORTED_REASONING_EFFORT, ZHIPU_RESULT_TOKENS @@ -733,6 +734,13 @@ export class OpenAIAPIClient extends OpenAIBaseClient< ...modalities, // groq 有不同的 service tier 配置,不符合 openai 接口类型 service_tier: this.getServiceTier(model) as OpenAIServiceTier, + ...(isSupportVerbosityModel(model) + ? { + text: { + verbosity: this.getVerbosity(model) + } + } + : {}), ...this.getProviderSpecificParameters(assistant, model), ...reasoningEffort, ...getOpenAIWebSearchParams(model, enableWebSearch), diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts index 0f72887196..cfbfdfd9df 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts @@ -520,7 +520,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< ...(isSupportVerbosityModel(model) ? { text: { - verbosity: this.getVerbosity() + verbosity: this.getVerbosity(model) } } : {}), diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 2d9f40329d..128a0f5269 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -1,6 +1,12 @@ import { baseProviderIdSchema, customProviderIdSchema } from '@cherrystudio/ai-core/provider' import { loggerService } from '@logger' -import { isOpenAIModel, isQwenMTModel, isSupportFlexServiceTierModel } from '@renderer/config/models' +import { + getModelSupportedVerbosity, + isOpenAIModel, + isQwenMTModel, + isSupportFlexServiceTierModel, + isSupportVerbosityModel +} from '@renderer/config/models' import { isSupportServiceTierProvider } from '@renderer/config/providers' import { mapLanguageToQwenMTModel } from '@renderer/config/translate' import type { Assistant, Model, Provider } from '@renderer/types' @@ -191,6 +197,23 @@ function buildOpenAIProviderOptions( ...reasoningParams } } + + if (isSupportVerbosityModel(model)) { + const state = window.store?.getState() + const userVerbosity = state?.settings?.openAI?.verbosity + + if (userVerbosity && ['low', 'medium', 'high'].includes(userVerbosity)) { + const supportedVerbosity = getModelSupportedVerbosity(model) + // Use user's verbosity if supported, otherwise use the first supported option + const verbosity = supportedVerbosity.includes(userVerbosity) ? userVerbosity : supportedVerbosity[0] + + providerOptions = { + ...providerOptions, + textVerbosity: verbosity + } + } + } + return providerOptions } diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index d0b6f1df25..dfe084179c 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -1,3 +1,7 @@ +import type { BedrockProviderOptions } from '@ai-sdk/amazon-bedrock' +import type { AnthropicProviderOptions } from '@ai-sdk/anthropic' +import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { XaiProviderOptions } from '@ai-sdk/xai' import { loggerService } from '@logger' import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant' import { @@ -7,6 +11,7 @@ import { isDeepSeekHybridInferenceModel, isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, + isGPT51SeriesModel, isGrok4FastReasoningModel, isGrokReasoningModel, isOpenAIDeepResearchModel, @@ -56,13 +61,20 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin } const reasoningEffort = assistant?.settings?.reasoning_effort - if (!reasoningEffort) { + // Handle undefined and 'none' reasoningEffort. + // TODO: They should be separated. + if (!reasoningEffort || reasoningEffort === 'none') { // openrouter: use reasoning if (model.provider === SystemProviderIds.openrouter) { // Don't disable reasoning for Gemini models that support thinking tokens if (isSupportedThinkingTokenGeminiModel(model) && !GEMINI_FLASH_MODEL_REGEX.test(model.id)) { return {} } + // 'none' is not an available value for effort for now. + // I think they should resolve this issue soon, so I'll just go ahead and use this value. + if (isGPT51SeriesModel(model) && reasoningEffort === 'none') { + return { reasoning: { effort: 'none' } } + } // Don't disable reasoning for models that require it if ( isGrokReasoningModel(model) || @@ -117,6 +129,13 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin return { thinking: { type: 'disabled' } } } + // Specially for GPT-5.1. Suppose this is a OpenAI Compatible provider + if (isGPT51SeriesModel(model) && reasoningEffort === 'none') { + return { + reasoningEffort: 'none' + } + } + return {} } @@ -371,7 +390,7 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re export function getAnthropicThinkingBudget(assistant: Assistant, model: Model): number { const { maxTokens, reasoning_effort: reasoningEffort } = getAssistantSettings(assistant) - if (reasoningEffort === undefined) { + if (reasoningEffort === undefined || reasoningEffort === 'none') { return 0 } const effortRatio = EFFORT_RATIO[reasoningEffort] @@ -393,14 +412,17 @@ export function getAnthropicThinkingBudget(assistant: Assistant, model: Model): * 获取 Anthropic 推理参数 * 从 AnthropicAPIClient 中提取的逻辑 */ -export function getAnthropicReasoningParams(assistant: Assistant, model: Model): Record { +export function getAnthropicReasoningParams( + assistant: Assistant, + model: Model +): Pick { if (!isReasoningModel(model)) { return {} } const reasoningEffort = assistant?.settings?.reasoning_effort - if (reasoningEffort === undefined) { + if (reasoningEffort === undefined || reasoningEffort === 'none') { return { thinking: { type: 'disabled' @@ -429,7 +451,10 @@ export function getAnthropicReasoningParams(assistant: Assistant, model: Model): * 注意:Gemini/GCP 端点所使用的 thinkingBudget 等参数应该按照驼峰命名法传递 * 而在 Google 官方提供的 OpenAI 兼容端点中则使用蛇形命名法 thinking_budget */ -export function getGeminiReasoningParams(assistant: Assistant, model: Model): Record { +export function getGeminiReasoningParams( + assistant: Assistant, + model: Model +): Pick { if (!isReasoningModel(model)) { return {} } @@ -438,7 +463,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re // Gemini 推理参数 if (isSupportedThinkingTokenGeminiModel(model)) { - if (reasoningEffort === undefined) { + if (reasoningEffort === undefined || reasoningEffort === 'none') { return { thinkingConfig: { includeThoughts: false, @@ -478,27 +503,35 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re * @param model - The model being used * @returns XAI-specific reasoning parameters */ -export function getXAIReasoningParams(assistant: Assistant, model: Model): Record { +export function getXAIReasoningParams(assistant: Assistant, model: Model): Pick { if (!isSupportedReasoningEffortGrokModel(model)) { return {} } const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant) - if (!reasoningEffort) { + if (!reasoningEffort || reasoningEffort === 'none') { return {} } - // For XAI provider Grok models, use reasoningEffort parameter directly - return { - reasoningEffort + switch (reasoningEffort) { + case 'auto': + case 'minimal': + case 'medium': + return { reasoningEffort: 'low' } + case 'low': + case 'high': + return { reasoningEffort } } } /** * Get Bedrock reasoning parameters */ -export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record { +export function getBedrockReasoningParams( + assistant: Assistant, + model: Model +): Pick { if (!isReasoningModel(model)) { return {} } @@ -509,6 +542,14 @@ export function getBedrockReasoningParams(assistant: Assistant, model: Model): R return {} } + if (reasoningEffort === 'none') { + return { + reasoningConfig: { + type: 'disabled' + } + } + } + // Only apply thinking budget for Claude reasoning models if (!isSupportedThinkingTokenClaudeModel(model)) { return {} diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 36b2954875..cc5449f819 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -8,7 +8,7 @@ import type { import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils' import { isEmbeddingModel, isRerankModel } from './embedding' -import { isGPT5SeriesModel, isGPT51SeriesModel } from './utils' +import { isGPT5ProModel, isGPT5SeriesModel, isGPT51SeriesModel } from './utils' import { isTextToImageModel } from './vision' import { GEMINI_FLASH_MODEL_REGEX, isOpenAIDeepResearchModel } from './websearch' @@ -26,6 +26,7 @@ export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = { gpt5_codex: ['low', 'medium', 'high'] as const, gpt5_1: ['none', 'low', 'medium', 'high'] as const, gpt5_1_codex: ['none', 'medium', 'high'] as const, + gpt5pro: ['high'] as const, grok: ['low', 'high'] as const, grok4_fast: ['auto'] as const, gemini: ['low', 'medium', 'high', 'auto'] as const, @@ -47,6 +48,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = { o: MODEL_SUPPORTED_REASONING_EFFORT.o, openai_deep_research: MODEL_SUPPORTED_REASONING_EFFORT.openai_deep_research, gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const, + gpt5pro: MODEL_SUPPORTED_REASONING_EFFORT.gpt5pro, gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex, gpt5_1: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_1, gpt5_1_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_1_codex, @@ -90,6 +92,9 @@ const _getThinkModelType = (model: Model): ThinkingModelType => { thinkingModelType = 'gpt5_codex' } else { thinkingModelType = 'gpt5' + if (isGPT5ProModel(model)) { + thinkingModelType = 'gpt5pro' + } } } else if (isSupportedReasoningEffortOpenAIModel(model)) { thinkingModelType = 'o' diff --git a/src/renderer/src/config/models/utils.ts b/src/renderer/src/config/models/utils.ts index 4197a516b3..7fb7c61362 100644 --- a/src/renderer/src/config/models/utils.ts +++ b/src/renderer/src/config/models/utils.ts @@ -240,6 +240,21 @@ export const isGPT51SeriesModel = (model: Model) => { return modelId.includes('gpt-5.1') } +// GPT-5 verbosity configuration +// gpt-5-pro only supports 'high', other GPT-5 models support all levels +export const MODEL_SUPPORTED_VERBOSITY: Record = { + 'gpt-5-pro': ['high'], + default: ['low', 'medium', 'high'] +} + +export const getModelSupportedVerbosity = (model: Model): ('low' | 'medium' | 'high')[] => { + const modelId = getLowerBaseModelName(model.id) + if (modelId.includes('gpt-5-pro')) { + return MODEL_SUPPORTED_VERBOSITY['gpt-5-pro'] + } + return MODEL_SUPPORTED_VERBOSITY.default +} + export const isGeminiModel = (model: Model) => { const modelId = getLowerBaseModelName(model.id) return modelId.includes('gemini') @@ -256,3 +271,8 @@ export const ZHIPU_RESULT_TOKENS = ['<|begin_of_box|>', '<|end_of_box|>'] as con export const agentModelFilter = (model: Model): boolean => { return !isEmbeddingModel(model) && !isRerankModel(model) && !isTextToImageModel(model) } + +export const isGPT5ProModel = (model: Model) => { + const modelId = getLowerBaseModelName(model.id) + return modelId.includes('gpt-5-pro') +} diff --git a/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx index e752f450b5..2960724183 100644 --- a/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx +++ b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx @@ -1,5 +1,6 @@ import Selector from '@renderer/components/Selector' import { + getModelSupportedVerbosity, isSupportedReasoningEffortOpenAIModel, isSupportFlexServiceTierModel, isSupportVerbosityModel @@ -80,20 +81,24 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti } ] - const verbosityOptions = [ - { - value: 'low', - label: t('settings.openai.verbosity.low') - }, - { - value: 'medium', - label: t('settings.openai.verbosity.medium') - }, - { - value: 'high', - label: t('settings.openai.verbosity.high') - } - ] + const verbosityOptions = useMemo(() => { + const allOptions = [ + { + value: 'low', + label: t('settings.openai.verbosity.low') + }, + { + value: 'medium', + label: t('settings.openai.verbosity.medium') + }, + { + value: 'high', + label: t('settings.openai.verbosity.high') + } + ] + const supportedVerbosityLevels = getModelSupportedVerbosity(model) + return allOptions.filter((option) => supportedVerbosityLevels.includes(option.value as any)) + }, [model, t]) const serviceTierOptions = useMemo(() => { let baseOptions: { value: ServiceTier; label: string }[] @@ -155,6 +160,15 @@ const OpenAISettingsGroup: FC = ({ model, providerId, SettingGroup, Setti } }, [provider.id, serviceTierMode, serviceTierOptions, setServiceTierMode]) + useEffect(() => { + if (verbosity && !verbosityOptions.some((option) => option.value === verbosity)) { + const supportedVerbosityLevels = getModelSupportedVerbosity(model) + // Default to the highest supported verbosity level + const defaultVerbosity = supportedVerbosityLevels[supportedVerbosityLevels.length - 1] + setVerbosity(defaultVerbosity) + } + }, [model, verbosity, verbosityOptions, setVerbosity]) + if (!isOpenAIReasoning && !isSupportServiceTier && !isSupportVerbosity) { return null } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index eb5d2fa1f5..01d654fdb2 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -86,6 +86,7 @@ const ThinkModelTypes = [ 'gpt5_1', 'gpt5_codex', 'gpt5_1_codex', + 'gpt5pro', 'grok', 'grok4_fast', 'gemini', @@ -113,7 +114,7 @@ export function isThinkModelType(type: string): type is ThinkingModelType { } export const EFFORT_RATIO: EffortRatio = { - none: 0, + none: 0.01, minimal: 0.05, low: 0.05, medium: 0.5, diff --git a/src/renderer/src/types/sdk.ts b/src/renderer/src/types/sdk.ts index 66e6b3627a..8e2af073a7 100644 --- a/src/renderer/src/types/sdk.ts +++ b/src/renderer/src/types/sdk.ts @@ -126,6 +126,10 @@ export type OpenAIExtraBody = { source_lang: 'auto' target_lang: string } + // for gpt-5 series models verbosity control + text?: { + verbosity?: 'low' | 'medium' | 'high' + } } // image is for openrouter. audio is ignored for now export type OpenAIModality = OpenAI.ChatCompletionModality | 'image' diff --git a/yarn.lock b/yarn.lock index e7a6944f56..dc6f25823e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,7 +102,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.44": +"@ai-sdk/anthropic@npm:2.0.44, @ai-sdk/anthropic@npm:^2.0.44": version: 2.0.44 resolution: "@ai-sdk/anthropic@npm:2.0.44" dependencies: @@ -206,6 +206,18 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/google@npm:^2.0.32": + version: 2.0.32 + resolution: "@ai-sdk/google@npm:2.0.32" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.17" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/052de16f1f66188e126168c8a9cc903448104528c7e44d6867bbf555c9067b9d6d44a4c4e0e014838156ba39095cb417f1b76363eb65212ca4d005f3651e58d2 + languageName: node + linkType: hard + "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch": version: 2.0.31 resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch::version=2.0.31&hash=9f3835" @@ -9891,8 +9903,10 @@ __metadata: "@agentic/searxng": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3" "@ai-sdk/amazon-bedrock": "npm:^3.0.53" + "@ai-sdk/anthropic": "npm:^2.0.44" "@ai-sdk/cerebras": "npm:^1.0.31" "@ai-sdk/gateway": "npm:^2.0.9" + "@ai-sdk/google": "npm:^2.0.32" "@ai-sdk/google-vertex": "npm:^3.0.62" "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch" "@ai-sdk/mistral": "npm:^2.0.23"