Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -199,15 +200,15 @@ export abstract class BaseApiClient<
}

public getTemperature(assistant: Assistant, model: Model): number | undefined {
if (isNotSupportTemperatureAndTopP(model)) {
if (!isSupportTemperatureModel(model)) {
return undefined
}
const assistantSettings = getAssistantSettings(assistant)
return assistantSettings?.enableTemperature ? assistantSettings?.temperature : undefined
}

public getTopP(assistant: Assistant, model: Model): number | undefined {
if (isNotSupportTemperatureAndTopP(model)) {
if (!isSupportTopPModel(model)) {
return undefined
}
const assistantSettings = getAssistantSettings(assistant)
Expand Down
59 changes: 40 additions & 19 deletions src/renderer/src/aiCore/prepareParams/modelParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
101 changes: 90 additions & 11 deletions src/renderer/src/config/models/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import {
isGenerateImageModels,
isMaxTemperatureOneModel,
isNotSupportSystemMessageModel,
isNotSupportTemperatureAndTopP,
isNotSupportTextDeltaModel,
isSupportedFlexServiceTier,
isSupportedModel,
isSupportFlexServiceTierModel,
isSupportTemperatureModel,
isSupportTopPModel,
isTemperatureTopPMutuallyExclusiveModel,
isVisionModels,
isZhipuModel
} from '../utils'
Expand Down Expand Up @@ -269,27 +271,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(isSupportTemperatureModel(model)).toBe(false)
})

it('returns true for open weight models', () => {
const openWeight = createModel({ id: 'gpt-oss-debug' })
expect(isSupportTemperatureModel(openWeight)).toBe(true)
})

it('returns false for chat-only models', () => {
const chatOnly = createModel({ id: 'o1-preview' })
expect(isSupportTemperatureModel(chatOnly)).toBe(false)
})

it('returns false for Qwen MT models', () => {
const qwenMt = createModel({ id: 'qwen-mt-large', provider: 'aliyun' })
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(isNotSupportTemperatureAndTopP(model)).toBe(true)
expect(isSupportTopPModel(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(isSupportTopPModel(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(isSupportTopPModel(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(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)
})
})
})
Expand Down
70 changes: 61 additions & 9 deletions src/renderer/src/config/models/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isOpenAIReasoningModel
} 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')
Expand Down Expand Up @@ -41,20 +42,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 {
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/services/AssistantService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Loading