diff --git a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts index 92f24b4abe..5d435b9074 100644 --- a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts @@ -2,9 +2,10 @@ import { loggerService } from '@logger' import { getModelSupportedVerbosity, isFunctionCallingModel, - isNotSupportTemperatureAndTopP, isOpenAIModel, - isSupportFlexServiceTierModel + isSupportFlexServiceTierModel, + isSupportTemperatureModel, + isSupportTopPModel } from '@renderer/config/models' import { REFERENCE_PROMPT } from '@renderer/config/prompts' import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio' @@ -200,7 +201,7 @@ export abstract class BaseApiClient< } public getTemperature(assistant: Assistant, model: Model): number | undefined { - if (isNotSupportTemperatureAndTopP(model)) { + if (!isSupportTemperatureModel(model)) { return undefined } const assistantSettings = getAssistantSettings(assistant) @@ -208,7 +209,7 @@ export abstract class BaseApiClient< } public getTopP(assistant: Assistant, model: Model): number | undefined { - if (isNotSupportTemperatureAndTopP(model)) { + if (!isSupportTopPModel(model)) { return undefined } const assistantSettings = getAssistantSettings(assistant) diff --git a/src/renderer/src/aiCore/prepareParams/modelParameters.ts b/src/renderer/src/aiCore/prepareParams/modelParameters.ts index 8a1d53a754..34c3418287 100644 --- a/src/renderer/src/aiCore/prepareParams/modelParameters.ts +++ b/src/renderer/src/aiCore/prepareParams/modelParameters.ts @@ -4,60 +4,81 @@ */ import { - isClaude45ReasoningModel, isClaudeReasoningModel, isMaxTemperatureOneModel, - isNotSupportTemperatureAndTopP, isSupportedFlexServiceTier, - isSupportedThinkingTokenClaudeModel + isSupportedThinkingTokenClaudeModel, + isSupportTemperatureModel, + isSupportTopPModel, + isTemperatureTopPMutuallyExclusiveModel } from '@renderer/config/models' -import { getAssistantSettings, getProviderByModel } from '@renderer/services/AssistantService' +import { + DEFAULT_ASSISTANT_SETTINGS, + getAssistantSettings, + getProviderByModel +} from '@renderer/services/AssistantService' import type { Assistant, Model } from '@renderer/types' import { defaultTimeout } from '@shared/config/constant' import { getAnthropicThinkingBudget } from '../utils/reasoning' /** - * Claude 4.5 推理模型: - * - 只启用 temperature → 使用 temperature - * - 只启用 top_p → 使用 top_p - * - 同时启用 → temperature 生效,top_p 被忽略 - * - 都不启用 → 都不使用 - * 获取温度参数 + * Retrieves the temperature parameter, adapting it based on assistant.settings and model capabilities. + * - Disabled for Claude reasoning models when reasoning effort is set. + * - Disabled for models that do not support temperature. + * - Disabled for Claude 4.5 reasoning models when TopP is enabled and temperature is disabled. + * Otherwise, returns the temperature value if the assistant has temperature enabled. */ export function getTemperature(assistant: Assistant, model: Model): number | undefined { if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } + + if (!isSupportTemperatureModel(model)) { + return undefined + } + if ( - isNotSupportTemperatureAndTopP(model) || - (isClaude45ReasoningModel(model) && assistant.settings?.enableTopP && !assistant.settings?.enableTemperature) + isTemperatureTopPMutuallyExclusiveModel(model) && + assistant.settings?.enableTopP && + !assistant.settings?.enableTemperature ) { return undefined } + const assistantSettings = getAssistantSettings(assistant) let temperature = assistantSettings?.temperature if (temperature && isMaxTemperatureOneModel(model)) { temperature = Math.min(1, temperature) } - return assistantSettings?.enableTemperature ? temperature : undefined + + // FIXME: assistant.settings.enableTemperature should be always a boolean value. + const enableTemperature = assistantSettings?.enableTemperature ?? DEFAULT_ASSISTANT_SETTINGS.enableTemperature + return enableTemperature ? temperature : undefined } /** - * 获取 TopP 参数 + * Retrieves the TopP parameter, adapting it based on assistant.settings and model capabilities. + * - Disabled for Claude reasoning models when reasoning effort is set. + * - Disabled for models that do not support TopP. + * - Disabled for Claude 4.5 reasoning models when temperature is explicitly enabled. + * Otherwise, returns the TopP value if the assistant has TopP enabled. */ export function getTopP(assistant: Assistant, model: Model): number | undefined { if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } - if ( - isNotSupportTemperatureAndTopP(model) || - (isClaude45ReasoningModel(model) && assistant.settings?.enableTemperature) - ) { + if (!isSupportTopPModel(model)) { return undefined } + if (isTemperatureTopPMutuallyExclusiveModel(model) && assistant.settings?.enableTemperature) { + return undefined + } + const assistantSettings = getAssistantSettings(assistant) - return assistantSettings?.enableTopP ? assistantSettings?.topP : undefined + // FIXME: assistant.settings.enableTopP should be always a boolean value. + const enableTopP = assistantSettings.enableTopP ?? DEFAULT_ASSISTANT_SETTINGS.enableTopP + return enableTopP ? assistantSettings?.topP : undefined } /** diff --git a/src/renderer/src/config/models/__tests__/utils.test.ts b/src/renderer/src/config/models/__tests__/utils.test.ts index ae4e33875c..042673b75d 100644 --- a/src/renderer/src/config/models/__tests__/utils.test.ts +++ b/src/renderer/src/config/models/__tests__/utils.test.ts @@ -25,11 +25,13 @@ import { isGenerateImageModels, isMaxTemperatureOneModel, isNotSupportSystemMessageModel, - isNotSupportTemperatureAndTopP, isNotSupportTextDeltaModel, isSupportedFlexServiceTier, isSupportedModel, isSupportFlexServiceTierModel, + isSupportTemperatureModel, + isSupportTopPModel, + isTemperatureTopPMutuallyExclusiveModel, isVisionModels, isZhipuModel } from '../utils' @@ -273,27 +275,104 @@ describe('model utils', () => { }) describe('Temperature and top-p support', () => { - describe('isNotSupportTemperatureAndTopP', () => { - it('returns true for reasoning models', () => { + describe('isSupportTemperatureModel', () => { + it('returns false for reasoning models (non-open weight)', () => { const model = createModel({ id: 'o1' }) reasoningMock.mockReturnValue(true) - expect(isNotSupportTemperatureAndTopP(model)).toBe(true) + expect(isSupportTemperatureModel(model)).toBe(false) }) - it('returns false for open weight models', () => { + it('returns true for open weight models', () => { const openWeight = createModel({ id: 'gpt-oss-debug' }) - expect(isNotSupportTemperatureAndTopP(openWeight)).toBe(false) + expect(isSupportTemperatureModel(openWeight)).toBe(true) }) - it('returns true for chat-only models without reasoning', () => { + it('returns false for chat-only models', () => { const chatOnly = createModel({ id: 'o1-preview' }) - reasoningMock.mockReturnValue(false) - expect(isNotSupportTemperatureAndTopP(chatOnly)).toBe(true) + expect(isSupportTemperatureModel(chatOnly)).toBe(false) }) - it('returns true for Qwen MT models', () => { + it('returns false for Qwen MT models', () => { const qwenMt = createModel({ id: 'qwen-mt-large', provider: 'aliyun' }) - expect(isNotSupportTemperatureAndTopP(qwenMt)).toBe(true) + expect(isSupportTemperatureModel(qwenMt)).toBe(false) + }) + + it('returns false for null/undefined models', () => { + expect(isSupportTemperatureModel(null)).toBe(false) + expect(isSupportTemperatureModel(undefined)).toBe(false) + }) + + it('returns true for regular GPT models', () => { + const model = createModel({ id: 'gpt-4' }) + expect(isSupportTemperatureModel(model)).toBe(true) + }) + }) + + describe('isSupportTopPModel', () => { + it('returns false for reasoning models (non-open weight)', () => { + const model = createModel({ id: 'o1' }) + reasoningMock.mockReturnValue(true) + expect(isSupportTopPModel(model)).toBe(false) + }) + + it('returns true for open weight models', () => { + const openWeight = createModel({ id: 'gpt-oss-debug' }) + expect(isSupportTopPModel(openWeight)).toBe(true) + }) + + it('returns false for chat-only models', () => { + const chatOnly = createModel({ id: 'o1-preview' }) + expect(isSupportTopPModel(chatOnly)).toBe(false) + }) + + it('returns false for Qwen MT models', () => { + const qwenMt = createModel({ id: 'qwen-mt-large', provider: 'aliyun' }) + expect(isSupportTopPModel(qwenMt)).toBe(false) + }) + + it('returns false for null/undefined models', () => { + expect(isSupportTopPModel(null)).toBe(false) + expect(isSupportTopPModel(undefined)).toBe(false) + }) + + it('returns true for regular GPT models', () => { + const model = createModel({ id: 'gpt-4' }) + expect(isSupportTopPModel(model)).toBe(true) + }) + }) + + describe('isTemperatureTopPMutuallyExclusiveModel', () => { + it('returns true for Claude 4.5 reasoning models', () => { + const claude45Sonnet = createModel({ id: 'claude-sonnet-4.5-20250514' }) + expect(isTemperatureTopPMutuallyExclusiveModel(claude45Sonnet)).toBe(true) + + const claude45Opus = createModel({ id: 'claude-opus-4.5-20250514' }) + expect(isTemperatureTopPMutuallyExclusiveModel(claude45Opus)).toBe(true) + }) + + it('returns false for Claude 4 models', () => { + const claude4Sonnet = createModel({ id: 'claude-sonnet-4-20250514' }) + expect(isTemperatureTopPMutuallyExclusiveModel(claude4Sonnet)).toBe(false) + }) + + it('returns false for Claude 3.x models', () => { + const claude35Sonnet = createModel({ id: 'claude-3-5-sonnet-20241022' }) + expect(isTemperatureTopPMutuallyExclusiveModel(claude35Sonnet)).toBe(false) + + const claude3Opus = createModel({ id: 'claude-3-opus-20240229' }) + expect(isTemperatureTopPMutuallyExclusiveModel(claude3Opus)).toBe(false) + }) + + it('returns false for other AI models', () => { + expect(isTemperatureTopPMutuallyExclusiveModel(createModel({ id: 'gpt-4o' }))).toBe(false) + expect(isTemperatureTopPMutuallyExclusiveModel(createModel({ id: 'o1' }))).toBe(false) + expect(isTemperatureTopPMutuallyExclusiveModel(createModel({ id: 'gemini-2.0-flash' }))).toBe(false) + expect(isTemperatureTopPMutuallyExclusiveModel(createModel({ id: 'qwen-max' }))).toBe(false) + }) + + it('returns false for null/undefined models', () => { + expect(isTemperatureTopPMutuallyExclusiveModel(null)).toBe(false) + expect(isTemperatureTopPMutuallyExclusiveModel(undefined)).toBe(false) }) }) }) diff --git a/src/renderer/src/config/models/utils.ts b/src/renderer/src/config/models/utils.ts index 625a5b5c63..9ae8defa7a 100644 --- a/src/renderer/src/config/models/utils.ts +++ b/src/renderer/src/config/models/utils.ts @@ -14,6 +14,7 @@ import { isSupportVerbosityModel } from './openai' import { isQwenMTModel } from './qwen' +import { isClaude45ReasoningModel } from './reasoning' import { isGenerateImageModel, isTextToImageModel, isVisionModel } from './vision' export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini.*-flash.*$', 'i') @@ -42,20 +43,71 @@ export function isSupportedModel(model: OpenAI.Models.Model): boolean { return !NOT_SUPPORTED_REGEX.test(modelId) } -export function isNotSupportTemperatureAndTopP(model: Model): boolean { +/** + * Check if the model supports temperature parameter + * @param model - The model to check + * @returns true if the model supports temperature parameter + */ +export function isSupportTemperatureModel(model: Model | undefined | null): boolean { if (!model) { - return true + return false } - if ( - (isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) || - isOpenAIChatCompletionOnlyModel(model) || - isQwenMTModel(model) - ) { - return true + // OpenAI reasoning models (except open weight) don't support temperature + if (isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) { + return false } - return false + // OpenAI chat completion only models don't support temperature + if (isOpenAIChatCompletionOnlyModel(model)) { + return false + } + + // Qwen MT models don't support temperature + if (isQwenMTModel(model)) { + return false + } + + return true +} + +/** + * Check if the model supports top_p parameter + * @param model - The model to check + * @returns true if the model supports top_p parameter + */ +export function isSupportTopPModel(model: Model | undefined | null): boolean { + if (!model) { + return false + } + + // OpenAI reasoning models (except open weight) don't support top_p + if (isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) { + return false + } + + // OpenAI chat completion only models don't support top_p + if (isOpenAIChatCompletionOnlyModel(model)) { + return false + } + + // Qwen MT models don't support top_p + if (isQwenMTModel(model)) { + return false + } + + return true +} + +/** + * Check if the model enforces mutual exclusivity between temperature and top_p parameters. + * Currently only Claude 4.5 reasoning models require this constraint. + * @param model - The model to check + * @returns true if temperature and top_p are mutually exclusive for this model + */ +export function isTemperatureTopPMutuallyExclusiveModel(model: Model | undefined | null): boolean { + if (!model) return false + return isClaude45ReasoningModel(model) } export function isGemmaModel(model?: Model): boolean { diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 96881c56b6..589fd7b6c8 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -27,7 +27,7 @@ import { uuid } from '@renderer/utils' const logger = loggerService.withContext('AssistantService') -export const DEFAULT_ASSISTANT_SETTINGS: AssistantSettings = { +export const DEFAULT_ASSISTANT_SETTINGS = { temperature: DEFAULT_TEMPERATURE, enableTemperature: true, contextCount: DEFAULT_CONTEXTCOUNT, @@ -39,7 +39,7 @@ export const DEFAULT_ASSISTANT_SETTINGS: AssistantSettings = { // It would gracefully fallback to prompt if not supported by model. toolUseMode: 'function', customParameters: [] -} as const +} as const satisfies AssistantSettings export function getDefaultAssistant(): Assistant { return {