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;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1498,6 +1498,7 @@ export const CreateTeamDialog = ({
|
|||
selectedEffort,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
|
||||
})
|
||||
: {
|
||||
nextEffort: selectedEffort,
|
||||
|
|
|
|||
|
|
@ -1073,6 +1073,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
selectedEffort,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
|
||||
})
|
||||
: {
|
||||
nextEffort: selectedEffort,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue