diff --git a/desktop/src/components/controls/ModelSelector.test.tsx b/desktop/src/components/controls/ModelSelector.test.tsx index 84c318c8..ffc012d5 100644 --- a/desktop/src/components/controls/ModelSelector.test.tsx +++ b/desktop/src/components/controls/ModelSelector.test.tsx @@ -323,12 +323,16 @@ describe('ModelSelector', () => { name: 'GPT-5.3 Codex', description: 'Best for coding and agentic work', context: '', + defaultReasoningEffort: 'medium', + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], }, { id: 'gpt-5.5', name: 'GPT-5.5', description: 'Latest general-purpose model', context: '', + defaultReasoningEffort: 'medium', + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], }, ] const setSessionRuntime = vi.fn() @@ -363,13 +367,77 @@ describe('ModelSelector', () => { expect(useSessionRuntimeStore.getState().selections['session-openai']).toEqual({ providerId: OPENAI_OFFICIAL_PROVIDER_ID, modelId: 'gpt-5.5', - effortLevel: 'max', + effortLevel: 'medium', }) expect(setSessionRuntime).toHaveBeenCalledWith('session-openai', { providerId: OPENAI_OFFICIAL_PROVIDER_ID, modelId: 'gpt-5.5', + effortLevel: 'medium', + }) + }) + + it('uses each ChatGPT model reasoning catalog and resets unsupported effort to its default', async () => { + const openAIModels: ModelInfo[] = [ + { + id: 'gpt-5.6-sol', + name: 'GPT-5.6-Sol', + description: 'Frontier model', + context: '353400', + defaultReasoningEffort: 'low', + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'], + }, + { + id: 'gpt-5.5', + name: 'GPT-5.5', + description: 'General model', + context: '258400', + defaultReasoningEffort: 'medium', + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], + }, + ] + useHahaOpenAIOAuthStore.setState({ + status: { loggedIn: true, expiresAt: null, email: null, accountId: null }, + fetchStatus: async () => {}, + }) + useSettingsStore.setState({ + locale: 'en', + availableModels: openAIModels, + currentModel: openAIModels[0], + activeProviderName: 'ChatGPT Official', effortLevel: 'max', }) + useProviderStore.setState({ + providers: [], + activeId: OPENAI_OFFICIAL_PROVIDER_ID, + hasLoadedProviders: true, + isLoading: true, + }) + useSessionRuntimeStore.getState().setSelection('session-openai-effort', { + providerId: OPENAI_OFFICIAL_PROVIDER_ID, + modelId: 'gpt-5.6-sol', + effortLevel: 'max', + }) + + render() + + await clickByRole(/GPT-5\.6-Sol/i) + await clickByRole(/GPT-5\.5/) + + expect(useSessionRuntimeStore.getState().selections['session-openai-effort']).toEqual({ + providerId: OPENAI_OFFICIAL_PROVIDER_ID, + modelId: 'gpt-5.5', + effortLevel: 'medium', + }) + + await clickByRole(/GPT-5\.5/i) + expect(screen.queryByRole('button', { name: /^Max$/ })).not.toBeInTheDocument() + await clickByRole(/^X-High$/) + + expect(useSessionRuntimeStore.getState().selections['session-openai-effort']).toEqual({ + providerId: OPENAI_OFFICIAL_PROVIDER_ID, + modelId: 'gpt-5.5', + effortLevel: 'xhigh', + }) }) it('hides official provider sections when OAuth is not logged in', async () => { diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index 2b505dc4..d1afa6dc 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -12,7 +12,7 @@ import { DRAFT_RUNTIME_SELECTION_KEY, useSessionRuntimeStore } from '../../store import { useSettingsStore } from '../../stores/settingsStore' import type { SavedProvider } from '../../types/provider' import type { RuntimeSelection } from '../../types/runtime' -import type { EffortLevel, ModelInfo } from '../../types/settings' +import type { ModelInfo, ReasoningEffortLevel } from '../../types/settings' import { useMobileViewport } from '../../hooks/useMobileViewport' import { isDesktopRuntime } from '../../lib/desktopRuntime' import { resolveDefaultRuntimeSelection } from '../../lib/runtimeSelection' @@ -182,10 +182,11 @@ export const ModelSelector = forwardRef(function Mod const requestedProvidersRef = useRef(false) const requestedOAuthStatusRef = useRef(false) - const EFFORT_OPTIONS: { value: EffortLevel; label: string }[] = [ + const EFFORT_OPTIONS: { value: ReasoningEffortLevel; label: string }[] = [ { value: 'low', label: t('settings.general.effort.low') }, { value: 'medium', label: t('settings.general.effort.medium') }, { value: 'high', label: t('settings.general.effort.high') }, + { value: 'xhigh', label: t('settings.general.effort.xhigh') }, { value: 'max', label: t('settings.general.effort.max') }, ] @@ -344,7 +345,13 @@ export const ModelSelector = forwardRef(function Mod const buttonProviderLabel = isRuntimeScoped ? selectedProviderChoice?.providerName ?? activeProviderName ?? t('settings.providers.officialName') : null - const selectedRuntimeEffort = activeRuntimeSelection?.effortLevel ?? effortLevel + const selectedRuntimeEffort = activeRuntimeSelection?.effortLevel + ?? selectedRuntimeModel?.defaultReasoningEffort + ?? effortLevel + const supportedRuntimeEfforts = selectedRuntimeModel?.supportedReasoningEfforts + const runtimeEffortOptions = supportedRuntimeEfforts?.length + ? EFFORT_OPTIONS.filter((option) => supportedRuntimeEfforts.includes(option.value)) + : EFFORT_OPTIONS.filter((option) => option.value !== 'xhigh') const handleRuntimeSelect = (selection: RuntimeSelection) => { onRuntimeSelectionChange?.(selection) @@ -357,7 +364,7 @@ export const ModelSelector = forwardRef(function Mod setOpen(false) } - const handleRuntimeEffortSelect = (level: EffortLevel) => { + const handleRuntimeEffortSelect = (level: ReasoningEffortLevel) => { if (!activeRuntimeSelection) return handleRuntimeSelect({ ...activeRuntimeSelection, @@ -397,11 +404,20 @@ export const ModelSelector = forwardRef(function Mod return (