refactor: improve temperature and top_p parameter handling (#11663)

* refactor(model-params): split temperature and top_p support checks into separate functions

Replace deprecated isNotSupportTemperatureAndTopP with isSupportTemperatureModel and isSupportTopPModel
Add comprehensive tests for new model parameter support functions

* refactor(model-parameters): improve temperature and topP parameter handling

- Add fallback to DEFAULT_ASSISTANT_SETTINGS when enableTemperature/enableTopP is undefined
- Simplify conditional logic in parameter validation
- Update documentation to better explain parameter selection rules

* refactor(models): remove deprecated isNotSupportTemperatureAndTopP function

The function was marked as deprecated and its usage has been replaced by isSupportTemperatureModel and isSupportTopPModel. Also removed corresponding test cases.

* feat(models): add mutual exclusivity check for temperature and top_p

Add new utility function to enforce mutual exclusivity between temperature and top_p parameters for Claude 4.5 reasoning models. Update model parameter preparation logic to use this new check and add corresponding tests.
This commit is contained in:
Phantom 2025-12-08 11:26:44 +08:00 committed by GitHub
parent 1a737f5137
commit 9f7e47304d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 198 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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