From 98d11b260c5ea3dedf6c7ea8c7475e66ff7c580b Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 19 May 2026 21:27:50 +0300 Subject: [PATCH] fix(team): align Anthropic effort UI fallback --- .../domain/resolveAnthropicRuntimeProfile.ts | 25 +++-- .../team/dialogs/CreateTeamDialog.tsx | 1 + .../team/dialogs/LaunchTeamDialog.tsx | 1 + .../utils/__tests__/teamEffortOptions.test.ts | 105 ++++++++++++++++-- src/renderer/utils/teamEffortOptions.ts | 37 +++++- .../resolveAnthropicRuntimeProfile.test.ts | 53 +++++++++ 6 files changed, 199 insertions(+), 23 deletions(-) diff --git a/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts b/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts index d06fd793..67027607 100644 --- a/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts +++ b/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts @@ -246,6 +246,7 @@ export function reconcileAnthropicRuntimeSelections(params: { selectedEffort?: string | null; selectedFastMode?: TeamFastMode | null; providerFastModeDefault?: boolean; + runtimeCapabilities?: CliProviderRuntimeCapabilities | null; }): AnthropicRuntimeReconciliation { const selectedEffort = normalizeEffortLevel(params.selectedEffort ?? null); if (!hasCatalogTruth(params.selection)) { @@ -257,14 +258,22 @@ export function reconcileAnthropicRuntimeSelections(params: { }; } - const nextEffort = - selectedEffort && !params.selection.supportedEfforts.includes(selectedEffort) - ? '' - : (selectedEffort ?? ''); - const effortResetReason = - selectedEffort && nextEffort === '' - ? `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.` - : null; + let nextEffort: EffortLevel | '' = selectedEffort ?? ''; + let effortResetReason: string | null = null; + if (selectedEffort) { + const effortSupport = resolveAnthropicEffortSupport({ + selection: params.selection, + effort: selectedEffort, + runtimeCapabilities: params.runtimeCapabilities, + }); + if ( + effortSupport.kind === 'unsupported-by-catalog' || + effortSupport.kind === 'unsupported-by-runtime-capability' + ) { + nextEffort = ''; + effortResetReason = `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.`; + } + } const fastResolution = resolveAnthropicFastMode({ selection: params.selection, diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 99b8e7e9..f54c88b9 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -1498,6 +1498,7 @@ export const CreateTeamDialog = ({ selectedEffort, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, + runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities, }) : { nextEffort: selectedEffort, diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx index d6a2a25d..62db4765 100644 --- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx @@ -1073,6 +1073,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen selectedEffort, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, + runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities, }) : { nextEffort: selectedEffort, diff --git a/src/renderer/utils/__tests__/teamEffortOptions.test.ts b/src/renderer/utils/__tests__/teamEffortOptions.test.ts index 481a6236..445efdcd 100644 --- a/src/renderer/utils/__tests__/teamEffortOptions.test.ts +++ b/src/renderer/utils/__tests__/teamEffortOptions.test.ts @@ -10,7 +10,7 @@ function createProviderStatus( options: { source?: 'anthropic-models-api' | 'app-server' | 'static-fallback'; configPassthrough?: boolean; - runtimeValues?: CliProviderStatus['runtimeCapabilities']; + runtimeValues?: CliProviderStatus['runtimeCapabilities'] | null; } = {} ): CliProviderStatus { const source = @@ -40,14 +40,17 @@ function createProviderStatus( }, }, modelAvailability: [], - runtimeCapabilities: options.runtimeValues ?? { - modelCatalog: { dynamic: true, source }, - reasoningEffort: { - supported: true, - values: model.supportedReasoningEfforts, - configPassthrough: options.configPassthrough === true, - }, - }, + runtimeCapabilities: + options.runtimeValues === undefined + ? { + modelCatalog: { dynamic: true, source }, + reasoningEffort: { + supported: true, + values: model.supportedReasoningEfforts, + configPassthrough: options.configPassthrough === true, + }, + } + : options.runtimeValues, canLoginFromUi: true, capabilities: { teamLaunch: true, @@ -191,6 +194,90 @@ describe('team effort options', () => { ]); }); + it('shows fallback Anthropic effort options for known models while catalog truth is unavailable', () => { + expect( + getTeamEffortOptions({ + providerId: 'anthropic', + model: 'claude-opus-4-6[1m]', + providerStatus: { + providerId: 'anthropic', + displayName: 'Anthropic', + supported: true, + authenticated: true, + authMethod: 'claude.ai', + verificationState: 'verified', + models: ['claude-opus-4-6'], + modelCatalog: null, + modelAvailability: [], + runtimeCapabilities: null, + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + extensions: { + plugins: { status: 'supported', ownership: 'shared', reason: null }, + mcp: { status: 'supported', ownership: 'shared', reason: null }, + skills: { status: 'supported', ownership: 'shared', reason: null }, + apiKeys: { status: 'supported', ownership: 'shared', reason: null }, + }, + }, + }, + }) + ).toEqual([ + { value: '', label: 'Default' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'max', label: 'Max' }, + ]); + }); + + it('does not invent Anthropic effort options for unknown models without catalog truth', () => { + expect( + getTeamEffortOptions({ + providerId: 'anthropic', + model: 'claude-experimental-5', + providerStatus: null, + }) + ).toEqual([{ value: '', label: 'Default' }]); + }); + + it('shows known Anthropic effort options when catalog lacks the exact selected model entry', () => { + const providerStatus = createProviderStatus( + 'anthropic', + { + id: 'haiku', + launchModel: 'haiku', + displayName: 'Haiku 4.5', + hidden: false, + supportedReasoningEfforts: [], + defaultReasoningEffort: null, + inputModalities: ['text', 'image'], + supportsPersonality: false, + isDefault: true, + upgrade: false, + source: 'anthropic-models-api', + }, + { runtimeValues: null } + ); + + const presentation = getTeamEffortSelectorPresentation({ + providerId: 'anthropic', + model: 'claude-opus-4-6[1m]', + providerStatus, + }); + + expect(presentation.options).toEqual([ + { value: '', label: 'Default' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'max', label: 'Max' }, + ]); + expect(presentation.disabled).toBe(false); + expect(presentation.canValidateValue).toBe(false); + }); + it('shows only Default when the selected Anthropic model does not support effort', () => { const providerStatus = createProviderStatus('anthropic', { id: 'haiku', diff --git a/src/renderer/utils/teamEffortOptions.ts b/src/renderer/utils/teamEffortOptions.ts index d07ce7e9..6ebc78cc 100644 --- a/src/renderer/utils/teamEffortOptions.ts +++ b/src/renderer/utils/teamEffortOptions.ts @@ -1,9 +1,13 @@ -import { resolveAnthropicRuntimeSelection } from '@features/anthropic-runtime-profile/renderer'; +import { + resolveAnthropicEffortSupport, + resolveAnthropicRuntimeSelection, +} from '@features/anthropic-runtime-profile/renderer'; import type { CliProviderStatus, EffortLevel, TeamProviderId } from '@shared/types'; const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const; const SAFE_SHARED_EFFORTS = new Set(['low', 'medium', 'high']); +const ANTHROPIC_FALLBACK_EFFORTS: readonly EffortLevel[] = ['low', 'medium', 'high', 'max']; export const TEAM_EFFORT_LABELS: Record = { none: 'None', @@ -67,6 +71,22 @@ function normalizeEfforts( return candidateEfforts.filter((effort) => SAFE_SHARED_EFFORTS.has(effort)); } +function getAnthropicEffortsFromRuntimeOrFallback(params: { + providerStatus?: CliProviderStatus | null; + selection: ReturnType; +}): EffortLevel[] { + const runtimeEfforts = params.providerStatus?.runtimeCapabilities?.reasoningEffort?.values ?? []; + const candidateEfforts = runtimeEfforts.length > 0 ? runtimeEfforts : ANTHROPIC_FALLBACK_EFFORTS; + return candidateEfforts.filter( + (effort): effort is EffortLevel => + resolveAnthropicEffortSupport({ + selection: params.selection, + effort, + runtimeCapabilities: params.providerStatus?.runtimeCapabilities, + }).kind === 'supported' + ); +} + export function getTeamEffortOptions(params: { providerId?: TeamProviderId; model?: string; @@ -90,9 +110,15 @@ export function getTeamEffortOptions(params: { const defaultLabel = selection.defaultEffort ? `Default (${TEAM_EFFORT_LABELS[selection.defaultEffort]})` : 'Default'; + const effortValues = selection.catalogModel + ? selection.supportedEfforts + : getAnthropicEffortsFromRuntimeOrFallback({ + providerStatus: params.providerStatus, + selection, + }); return [ { value: '', label: defaultLabel }, - ...selection.supportedEfforts.map((effort) => ({ + ...effortValues.map((effort) => ({ value: effort, label: TEAM_EFFORT_LABELS[effort], })), @@ -163,17 +189,16 @@ export function getTeamEffortSelectorPresentation(params: { selectedModel: params.model, limitContext: params.limitContext === true, }); - const hasCatalogTruth = - selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable'; + const hasExactCatalogTruth = selection.catalogModel !== null; const supportsConfigurableEffort = selection.supportedEfforts.length > 0; - if (!hasCatalogTruth || supportsConfigurableEffort) { + if (!hasExactCatalogTruth || supportsConfigurableEffort) { return { options, disabled: false, helperText: defaultHelperText, unavailableText: null, - canValidateValue: hasCatalogTruth, + canValidateValue: hasExactCatalogTruth, }; } diff --git a/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts b/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts index 6203c9e0..997feb6d 100644 --- a/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts +++ b/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts @@ -312,6 +312,59 @@ describe('resolveAnthropicRuntimeProfile', () => { ).toBe('Anthropic runtime capability data is still loading.'); }); + it('does not reset effort when catalog exists but the exact known model entry is missing', () => { + const source = createAnthropicSource({ + defaultLaunchModel: 'haiku', + models: [ + { + id: 'haiku', + launchModel: 'haiku', + displayName: 'Haiku 4.5', + hidden: false, + supportedReasoningEfforts: [], + defaultReasoningEffort: null, + inputModalities: ['text', 'image'], + supportsFastMode: false, + supportsPersonality: false, + isDefault: true, + upgrade: false, + source: 'anthropic-models-api', + }, + ], + }); + const selection = resolveAnthropicRuntimeSelection({ + source: { + modelCatalog: source.modelCatalog, + runtimeCapabilities: null, + }, + selectedModel: 'claude-opus-4-6[1m]', + limitContext: false, + }); + + expect(selection.catalogModel).toBeNull(); + expect( + resolveAnthropicEffortSupport({ + selection, + effort: 'medium', + runtimeCapabilities: null, + }) + ).toEqual({ kind: 'supported', source: 'static-fallback' }); + expect( + reconcileAnthropicRuntimeSelections({ + selection, + selectedEffort: 'medium', + selectedFastMode: 'inherit', + providerFastModeDefault: false, + runtimeCapabilities: null, + }) + ).toEqual({ + nextEffort: 'medium', + effortResetReason: null, + nextFastMode: 'inherit', + fastModeResetReason: null, + }); + }); + it('allows known Opus 1M effort when catalog is unavailable but runtime capability passthrough is present', () => { const selection = resolveAnthropicRuntimeSelection({ source: {