mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-22 08:40:08 +08:00
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:
parent
1a737f5137
commit
9f7e47304d
@ -2,9 +2,10 @@ import { loggerService } from '@logger'
|
|||||||
import {
|
import {
|
||||||
getModelSupportedVerbosity,
|
getModelSupportedVerbosity,
|
||||||
isFunctionCallingModel,
|
isFunctionCallingModel,
|
||||||
isNotSupportTemperatureAndTopP,
|
|
||||||
isOpenAIModel,
|
isOpenAIModel,
|
||||||
isSupportFlexServiceTierModel
|
isSupportFlexServiceTierModel,
|
||||||
|
isSupportTemperatureModel,
|
||||||
|
isSupportTopPModel
|
||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import { REFERENCE_PROMPT } from '@renderer/config/prompts'
|
import { REFERENCE_PROMPT } from '@renderer/config/prompts'
|
||||||
import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio'
|
import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio'
|
||||||
@ -200,7 +201,7 @@ export abstract class BaseApiClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTemperature(assistant: Assistant, model: Model): number | undefined {
|
public getTemperature(assistant: Assistant, model: Model): number | undefined {
|
||||||
if (isNotSupportTemperatureAndTopP(model)) {
|
if (!isSupportTemperatureModel(model)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const assistantSettings = getAssistantSettings(assistant)
|
const assistantSettings = getAssistantSettings(assistant)
|
||||||
@ -208,7 +209,7 @@ export abstract class BaseApiClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTopP(assistant: Assistant, model: Model): number | undefined {
|
public getTopP(assistant: Assistant, model: Model): number | undefined {
|
||||||
if (isNotSupportTemperatureAndTopP(model)) {
|
if (!isSupportTopPModel(model)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const assistantSettings = getAssistantSettings(assistant)
|
const assistantSettings = getAssistantSettings(assistant)
|
||||||
|
|||||||
@ -4,60 +4,81 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isClaude45ReasoningModel,
|
|
||||||
isClaudeReasoningModel,
|
isClaudeReasoningModel,
|
||||||
isMaxTemperatureOneModel,
|
isMaxTemperatureOneModel,
|
||||||
isNotSupportTemperatureAndTopP,
|
|
||||||
isSupportedFlexServiceTier,
|
isSupportedFlexServiceTier,
|
||||||
isSupportedThinkingTokenClaudeModel
|
isSupportedThinkingTokenClaudeModel,
|
||||||
|
isSupportTemperatureModel,
|
||||||
|
isSupportTopPModel,
|
||||||
|
isTemperatureTopPMutuallyExclusiveModel
|
||||||
} from '@renderer/config/models'
|
} 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 type { Assistant, Model } from '@renderer/types'
|
||||||
import { defaultTimeout } from '@shared/config/constant'
|
import { defaultTimeout } from '@shared/config/constant'
|
||||||
|
|
||||||
import { getAnthropicThinkingBudget } from '../utils/reasoning'
|
import { getAnthropicThinkingBudget } from '../utils/reasoning'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude 4.5 推理模型:
|
* Retrieves the temperature parameter, adapting it based on assistant.settings and model capabilities.
|
||||||
* - 只启用 temperature → 使用 temperature
|
* - Disabled for Claude reasoning models when reasoning effort is set.
|
||||||
* - 只启用 top_p → 使用 top_p
|
* - Disabled for models that do not support temperature.
|
||||||
* - 同时启用 → temperature 生效,top_p 被忽略
|
* - 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 {
|
export function getTemperature(assistant: Assistant, model: Model): number | undefined {
|
||||||
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSupportTemperatureModel(model)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isNotSupportTemperatureAndTopP(model) ||
|
isTemperatureTopPMutuallyExclusiveModel(model) &&
|
||||||
(isClaude45ReasoningModel(model) && assistant.settings?.enableTopP && !assistant.settings?.enableTemperature)
|
assistant.settings?.enableTopP &&
|
||||||
|
!assistant.settings?.enableTemperature
|
||||||
) {
|
) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantSettings = getAssistantSettings(assistant)
|
const assistantSettings = getAssistantSettings(assistant)
|
||||||
let temperature = assistantSettings?.temperature
|
let temperature = assistantSettings?.temperature
|
||||||
if (temperature && isMaxTemperatureOneModel(model)) {
|
if (temperature && isMaxTemperatureOneModel(model)) {
|
||||||
temperature = Math.min(1, temperature)
|
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 {
|
export function getTopP(assistant: Assistant, model: Model): number | undefined {
|
||||||
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if (
|
if (!isSupportTopPModel(model)) {
|
||||||
isNotSupportTemperatureAndTopP(model) ||
|
|
||||||
(isClaude45ReasoningModel(model) && assistant.settings?.enableTemperature)
|
|
||||||
) {
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
if (isTemperatureTopPMutuallyExclusiveModel(model) && assistant.settings?.enableTemperature) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const assistantSettings = getAssistantSettings(assistant)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -25,11 +25,13 @@ import {
|
|||||||
isGenerateImageModels,
|
isGenerateImageModels,
|
||||||
isMaxTemperatureOneModel,
|
isMaxTemperatureOneModel,
|
||||||
isNotSupportSystemMessageModel,
|
isNotSupportSystemMessageModel,
|
||||||
isNotSupportTemperatureAndTopP,
|
|
||||||
isNotSupportTextDeltaModel,
|
isNotSupportTextDeltaModel,
|
||||||
isSupportedFlexServiceTier,
|
isSupportedFlexServiceTier,
|
||||||
isSupportedModel,
|
isSupportedModel,
|
||||||
isSupportFlexServiceTierModel,
|
isSupportFlexServiceTierModel,
|
||||||
|
isSupportTemperatureModel,
|
||||||
|
isSupportTopPModel,
|
||||||
|
isTemperatureTopPMutuallyExclusiveModel,
|
||||||
isVisionModels,
|
isVisionModels,
|
||||||
isZhipuModel
|
isZhipuModel
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
@ -273,27 +275,104 @@ describe('model utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Temperature and top-p support', () => {
|
describe('Temperature and top-p support', () => {
|
||||||
describe('isNotSupportTemperatureAndTopP', () => {
|
describe('isSupportTemperatureModel', () => {
|
||||||
it('returns true for reasoning models', () => {
|
it('returns false for reasoning models (non-open weight)', () => {
|
||||||
const model = createModel({ id: 'o1' })
|
const model = createModel({ id: 'o1' })
|
||||||
reasoningMock.mockReturnValue(true)
|
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' })
|
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' })
|
const chatOnly = createModel({ id: 'o1-preview' })
|
||||||
reasoningMock.mockReturnValue(false)
|
expect(isSupportTemperatureModel(chatOnly)).toBe(false)
|
||||||
expect(isNotSupportTemperatureAndTopP(chatOnly)).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns true for Qwen MT models', () => {
|
it('returns false for Qwen MT models', () => {
|
||||||
const qwenMt = createModel({ id: 'qwen-mt-large', provider: 'aliyun' })
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
isSupportVerbosityModel
|
isSupportVerbosityModel
|
||||||
} from './openai'
|
} from './openai'
|
||||||
import { isQwenMTModel } from './qwen'
|
import { isQwenMTModel } from './qwen'
|
||||||
|
import { isClaude45ReasoningModel } from './reasoning'
|
||||||
import { isGenerateImageModel, isTextToImageModel, isVisionModel } from './vision'
|
import { isGenerateImageModel, isTextToImageModel, isVisionModel } from './vision'
|
||||||
export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
|
export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
|
||||||
export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini.*-flash.*$', '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)
|
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) {
|
if (!model) {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// OpenAI reasoning models (except open weight) don't support temperature
|
||||||
(isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) ||
|
if (isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) {
|
||||||
isOpenAIChatCompletionOnlyModel(model) ||
|
return false
|
||||||
isQwenMTModel(model)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export function isGemmaModel(model?: Model): boolean {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { uuid } from '@renderer/utils'
|
|||||||
|
|
||||||
const logger = loggerService.withContext('AssistantService')
|
const logger = loggerService.withContext('AssistantService')
|
||||||
|
|
||||||
export const DEFAULT_ASSISTANT_SETTINGS: AssistantSettings = {
|
export const DEFAULT_ASSISTANT_SETTINGS = {
|
||||||
temperature: DEFAULT_TEMPERATURE,
|
temperature: DEFAULT_TEMPERATURE,
|
||||||
enableTemperature: true,
|
enableTemperature: true,
|
||||||
contextCount: DEFAULT_CONTEXTCOUNT,
|
contextCount: DEFAULT_CONTEXTCOUNT,
|
||||||
@ -39,7 +39,7 @@ export const DEFAULT_ASSISTANT_SETTINGS: AssistantSettings = {
|
|||||||
// It would gracefully fallback to prompt if not supported by model.
|
// It would gracefully fallback to prompt if not supported by model.
|
||||||
toolUseMode: 'function',
|
toolUseMode: 'function',
|
||||||
customParameters: []
|
customParameters: []
|
||||||
} as const
|
} as const satisfies AssistantSettings
|
||||||
|
|
||||||
export function getDefaultAssistant(): Assistant {
|
export function getDefaultAssistant(): Assistant {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user