fix(team): align Anthropic effort UI fallback
This commit is contained in:
parent
568c0f237e
commit
98d11b260c
6 changed files with 199 additions and 23 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1498,6 +1498,7 @@ export const CreateTeamDialog = ({
|
||||||
selectedEffort,
|
selectedEffort,
|
||||||
selectedFastMode,
|
selectedFastMode,
|
||||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||||
|
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
|
||||||
})
|
})
|
||||||
: {
|
: {
|
||||||
nextEffort: selectedEffort,
|
nextEffort: selectedEffort,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue