fix(team): align Anthropic effort UI fallback

This commit is contained in:
777genius 2026-05-19 21:27:50 +03:00
parent 568c0f237e
commit 98d11b260c
6 changed files with 199 additions and 23 deletions

View file

@ -246,6 +246,7 @@ export function reconcileAnthropicRuntimeSelections(params: {
selectedEffort?: string | null; selectedEffort?: string | null;
selectedFastMode?: TeamFastMode | null; selectedFastMode?: TeamFastMode | null;
providerFastModeDefault?: boolean; providerFastModeDefault?: boolean;
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
}): AnthropicRuntimeReconciliation { }): AnthropicRuntimeReconciliation {
const selectedEffort = normalizeEffortLevel(params.selectedEffort ?? null); const selectedEffort = normalizeEffortLevel(params.selectedEffort ?? null);
if (!hasCatalogTruth(params.selection)) { if (!hasCatalogTruth(params.selection)) {
@ -257,14 +258,22 @@ export function reconcileAnthropicRuntimeSelections(params: {
}; };
} }
const nextEffort = let nextEffort: EffortLevel | '' = selectedEffort ?? '';
selectedEffort && !params.selection.supportedEfforts.includes(selectedEffort) let effortResetReason: string | null = null;
? '' if (selectedEffort) {
: (selectedEffort ?? ''); const effortSupport = resolveAnthropicEffortSupport({
const effortResetReason = selection: params.selection,
selectedEffort && nextEffort === '' effort: selectedEffort,
? `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.` runtimeCapabilities: params.runtimeCapabilities,
: null; });
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({ const fastResolution = resolveAnthropicFastMode({
selection: params.selection, selection: params.selection,

View file

@ -1498,6 +1498,7 @@ export const CreateTeamDialog = ({
selectedEffort, selectedEffort,
selectedFastMode, selectedFastMode,
providerFastModeDefault: anthropicProviderFastModeDefault, providerFastModeDefault: anthropicProviderFastModeDefault,
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
}) })
: { : {
nextEffort: selectedEffort, nextEffort: selectedEffort,

View file

@ -1073,6 +1073,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedEffort, selectedEffort,
selectedFastMode, selectedFastMode,
providerFastModeDefault: anthropicProviderFastModeDefault, providerFastModeDefault: anthropicProviderFastModeDefault,
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
}) })
: { : {
nextEffort: selectedEffort, nextEffort: selectedEffort,

View file

@ -10,7 +10,7 @@ function createProviderStatus(
options: { options: {
source?: 'anthropic-models-api' | 'app-server' | 'static-fallback'; source?: 'anthropic-models-api' | 'app-server' | 'static-fallback';
configPassthrough?: boolean; configPassthrough?: boolean;
runtimeValues?: CliProviderStatus['runtimeCapabilities']; runtimeValues?: CliProviderStatus['runtimeCapabilities'] | null;
} = {} } = {}
): CliProviderStatus { ): CliProviderStatus {
const source = const source =
@ -40,14 +40,17 @@ function createProviderStatus(
}, },
}, },
modelAvailability: [], modelAvailability: [],
runtimeCapabilities: options.runtimeValues ?? { runtimeCapabilities:
modelCatalog: { dynamic: true, source }, options.runtimeValues === undefined
reasoningEffort: { ? {
supported: true, modelCatalog: { dynamic: true, source },
values: model.supportedReasoningEfforts, reasoningEffort: {
configPassthrough: options.configPassthrough === true, supported: true,
}, values: model.supportedReasoningEfforts,
}, configPassthrough: options.configPassthrough === true,
},
}
: options.runtimeValues,
canLoginFromUi: true, canLoginFromUi: true,
capabilities: { capabilities: {
teamLaunch: true, 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', () => { it('shows only Default when the selected Anthropic model does not support effort', () => {
const providerStatus = createProviderStatus('anthropic', { const providerStatus = createProviderStatus('anthropic', {
id: 'haiku', id: 'haiku',

View file

@ -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'; import type { CliProviderStatus, EffortLevel, TeamProviderId } from '@shared/types';
const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const; const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const;
const SAFE_SHARED_EFFORTS = new Set<EffortLevel>(['low', 'medium', 'high']); const SAFE_SHARED_EFFORTS = new Set<EffortLevel>(['low', 'medium', 'high']);
const ANTHROPIC_FALLBACK_EFFORTS: readonly EffortLevel[] = ['low', 'medium', 'high', 'max'];
export const TEAM_EFFORT_LABELS: Record<EffortLevel, string> = { export const TEAM_EFFORT_LABELS: Record<EffortLevel, string> = {
none: 'None', none: 'None',
@ -67,6 +71,22 @@ function normalizeEfforts(
return candidateEfforts.filter((effort) => SAFE_SHARED_EFFORTS.has(effort)); return candidateEfforts.filter((effort) => SAFE_SHARED_EFFORTS.has(effort));
} }
function getAnthropicEffortsFromRuntimeOrFallback(params: {
providerStatus?: CliProviderStatus | null;
selection: ReturnType<typeof resolveAnthropicRuntimeSelection>;
}): 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: { export function getTeamEffortOptions(params: {
providerId?: TeamProviderId; providerId?: TeamProviderId;
model?: string; model?: string;
@ -90,9 +110,15 @@ export function getTeamEffortOptions(params: {
const defaultLabel = selection.defaultEffort const defaultLabel = selection.defaultEffort
? `Default (${TEAM_EFFORT_LABELS[selection.defaultEffort]})` ? `Default (${TEAM_EFFORT_LABELS[selection.defaultEffort]})`
: 'Default'; : 'Default';
const effortValues = selection.catalogModel
? selection.supportedEfforts
: getAnthropicEffortsFromRuntimeOrFallback({
providerStatus: params.providerStatus,
selection,
});
return [ return [
{ value: '', label: defaultLabel }, { value: '', label: defaultLabel },
...selection.supportedEfforts.map((effort) => ({ ...effortValues.map((effort) => ({
value: effort, value: effort,
label: TEAM_EFFORT_LABELS[effort], label: TEAM_EFFORT_LABELS[effort],
})), })),
@ -163,17 +189,16 @@ export function getTeamEffortSelectorPresentation(params: {
selectedModel: params.model, selectedModel: params.model,
limitContext: params.limitContext === true, limitContext: params.limitContext === true,
}); });
const hasCatalogTruth = const hasExactCatalogTruth = selection.catalogModel !== null;
selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable';
const supportsConfigurableEffort = selection.supportedEfforts.length > 0; const supportsConfigurableEffort = selection.supportedEfforts.length > 0;
if (!hasCatalogTruth || supportsConfigurableEffort) { if (!hasExactCatalogTruth || supportsConfigurableEffort) {
return { return {
options, options,
disabled: false, disabled: false,
helperText: defaultHelperText, helperText: defaultHelperText,
unavailableText: null, unavailableText: null,
canValidateValue: hasCatalogTruth, canValidateValue: hasExactCatalogTruth,
}; };
} }

View file

@ -312,6 +312,59 @@ describe('resolveAnthropicRuntimeProfile', () => {
).toBe('Anthropic runtime capability data is still loading.'); ).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', () => { it('allows known Opus 1M effort when catalog is unavailable but runtime capability passthrough is present', () => {
const selection = resolveAnthropicRuntimeSelection({ const selection = resolveAnthropicRuntimeSelection({
source: { source: {