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;
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,

View file

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

View file

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

View file

@ -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',

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';
const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const;
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> = {
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<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: {
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,
};
}

View file

@ -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: {