feat(models): add Opus 4.8 selector support
This commit is contained in:
parent
e215c09af0
commit
61418cf2f2
10 changed files with 205 additions and 11 deletions
|
|
@ -95,6 +95,8 @@ function isKnownAnthropicReasoningModel(model: string | null | undefined): boole
|
||||||
return (
|
return (
|
||||||
normalized === 'opus' ||
|
normalized === 'opus' ||
|
||||||
normalized === 'sonnet' ||
|
normalized === 'sonnet' ||
|
||||||
|
normalized === 'claude-opus-4-8' ||
|
||||||
|
normalized.startsWith('claude-opus-4-8-') ||
|
||||||
normalized === 'claude-opus-4-7' ||
|
normalized === 'claude-opus-4-7' ||
|
||||||
normalized.startsWith('claude-opus-4-7-') ||
|
normalized.startsWith('claude-opus-4-7-') ||
|
||||||
normalized === 'claude-opus-4-6' ||
|
normalized === 'claude-opus-4-6' ||
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ const OPENCODE_MODEL_GRID_MAX_HEIGHT_PX = 400;
|
||||||
const OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD = 80;
|
const OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD = 80;
|
||||||
const OPENCODE_MODEL_GROUP_HEADING_ESTIMATE_PX = 28;
|
const OPENCODE_MODEL_GROUP_HEADING_ESTIMATE_PX = 28;
|
||||||
const OPENCODE_MODEL_ROW_ESTIMATE_PX = 92;
|
const OPENCODE_MODEL_ROW_ESTIMATE_PX = 92;
|
||||||
|
const ANTHROPIC_OPUS_48_NEW_BADGE_EXPIRES_AT_MS = Date.UTC(2026, 5, 12);
|
||||||
|
|
||||||
const PROVIDERS: ProviderDef[] = [
|
const PROVIDERS: ProviderDef[] = [
|
||||||
{ id: 'anthropic', label: 'Anthropic', comingSoon: false },
|
{ id: 'anthropic', label: 'Anthropic', comingSoon: false },
|
||||||
|
|
@ -421,6 +422,24 @@ function isFreeOpenCodeModelRoute(model: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAnthropicOpus48NewBadgeVisible(
|
||||||
|
providerId: TeamProviderId,
|
||||||
|
model: string,
|
||||||
|
nowMs = Date.now()
|
||||||
|
): boolean {
|
||||||
|
if (providerId !== 'anthropic' || nowMs >= ANTHROPIC_OPUS_48_NEW_BADGE_EXPIRES_AT_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = model.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized === 'opus' ||
|
||||||
|
normalized === 'opus[1m]' ||
|
||||||
|
normalized === 'claude-opus-4-8' ||
|
||||||
|
normalized === 'claude-opus-4-8[1m]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasFreeOpenCodeModelRoute(providerStatus: CliProviderStatus | null | undefined): boolean {
|
function hasFreeOpenCodeModelRoute(providerStatus: CliProviderStatus | null | undefined): boolean {
|
||||||
if (providerStatus?.providerId !== 'opencode') {
|
if (providerStatus?.providerId !== 'opencode') {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -825,13 +844,13 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
'anthropic',
|
'anthropic',
|
||||||
getAnthropicDefaultTeamModel(false),
|
getAnthropicDefaultTeamModel(false),
|
||||||
runtimeProviderStatus
|
runtimeProviderStatus
|
||||||
) ?? 'Opus 4.7 (1M)';
|
) ?? 'Opus 4.8 (1M)';
|
||||||
const defaultLimitedContextModel =
|
const defaultLimitedContextModel =
|
||||||
getRuntimeAwareProviderScopedTeamModelLabel(
|
getRuntimeAwareProviderScopedTeamModelLabel(
|
||||||
'anthropic',
|
'anthropic',
|
||||||
getAnthropicDefaultTeamModel(true),
|
getAnthropicDefaultTeamModel(true),
|
||||||
runtimeProviderStatus
|
runtimeProviderStatus
|
||||||
) ?? 'Opus 4.7';
|
) ?? 'Opus 4.8';
|
||||||
|
|
||||||
return t('modelSelector.defaultTooltip.anthropic', {
|
return t('modelSelector.defaultTooltip.anthropic', {
|
||||||
longContextModel: defaultLongContextModel,
|
longContextModel: defaultLongContextModel,
|
||||||
|
|
@ -1420,6 +1439,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const openCodeProofState = openCodeRouteMetadata?.proofState ?? null;
|
const openCodeProofState = openCodeRouteMetadata?.proofState ?? null;
|
||||||
const modelButtonTitle =
|
const modelButtonTitle =
|
||||||
modelStatusMessage ?? (opt.value === '' ? defaultModelTooltip : undefined);
|
modelStatusMessage ?? (opt.value === '' ? defaultModelTooltip : undefined);
|
||||||
|
const showNewRibbon = isAnthropicOpus48NewBadgeVisible(effectiveProviderId, opt.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -1429,7 +1449,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
aria-disabled={!modelSelectable}
|
aria-disabled={!modelSelectable}
|
||||||
title={modelButtonTitle}
|
title={modelButtonTitle}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-[44px] items-center justify-center gap-1.5 rounded-md border bg-[var(--color-surface)] px-3 py-2 text-center text-xs font-medium transition-[background-color,border-color,color,box-shadow] duration-150',
|
'relative flex min-h-[44px] items-center justify-center gap-1.5 overflow-hidden rounded-md border bg-[var(--color-surface)] px-3 py-2 text-center text-xs font-medium transition-[background-color,border-color,color,box-shadow] duration-150',
|
||||||
hasBlockingModelIssue && normalizedValue === opt.value
|
hasBlockingModelIssue && normalizedValue === opt.value
|
||||||
? 'border-red-500/60 bg-red-500/10 text-red-100 shadow-sm'
|
? 'border-red-500/60 bg-red-500/10 text-red-100 shadow-sm'
|
||||||
: hasBlockingModelIssue
|
: hasBlockingModelIssue
|
||||||
|
|
@ -1609,6 +1629,11 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
{showNewRibbon ? (
|
||||||
|
<span className="pointer-events-none absolute right-[-22px] top-1.5 w-[72px] rotate-45 border border-emerald-300/35 bg-emerald-400/15 py-0.5 text-center text-[8px] font-bold uppercase leading-none tracking-[0.14em] text-emerald-100 shadow-sm">
|
||||||
|
New
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,17 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANTHROPIC_ALIAS_LABELS = {
|
const ANTHROPIC_ALIAS_LABELS = {
|
||||||
opus: 'Opus 4.7',
|
opus: 'Opus 4.8',
|
||||||
sonnet: 'Sonnet 4.6',
|
sonnet: 'Sonnet 4.6',
|
||||||
haiku: 'Haiku 4.5',
|
haiku: 'Haiku 4.5',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const ANTHROPIC_VISIBLE_MODEL_FALLBACKS = ['claude-opus-4-7', 'claude-opus-4-7[1m]'] as const;
|
const ANTHROPIC_VISIBLE_MODEL_FALLBACKS = [
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-8[1m]',
|
||||||
|
'claude-opus-4-7',
|
||||||
|
'claude-opus-4-7[1m]',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const ANTHROPIC_MODEL_ORDER = [
|
const ANTHROPIC_MODEL_ORDER = [
|
||||||
'haiku',
|
'haiku',
|
||||||
|
|
@ -60,6 +65,8 @@ const ANTHROPIC_MODEL_ORDER = [
|
||||||
'claude-haiku-4-5',
|
'claude-haiku-4-5',
|
||||||
'opus',
|
'opus',
|
||||||
'opus[1m]',
|
'opus[1m]',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-8[1m]',
|
||||||
'claude-opus-4-7',
|
'claude-opus-4-7',
|
||||||
'claude-opus-4-7[1m]',
|
'claude-opus-4-7[1m]',
|
||||||
'claude-opus-4-6',
|
'claude-opus-4-6',
|
||||||
|
|
@ -73,6 +80,8 @@ const ANTHROPIC_MODEL_ORDER = [
|
||||||
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
...ANTHROPIC_ALIAS_LABELS,
|
...ANTHROPIC_ALIAS_LABELS,
|
||||||
|
'claude-opus-4-8': 'Opus 4.8',
|
||||||
|
'claude-opus-4-8[1m]': 'Opus 4.8 (1M)',
|
||||||
'claude-opus-4-7': 'Opus 4.7',
|
'claude-opus-4-7': 'Opus 4.7',
|
||||||
'claude-opus-4-7[1m]': 'Opus 4.7 (1M)',
|
'claude-opus-4-7[1m]': 'Opus 4.7 (1M)',
|
||||||
'claude-sonnet-4-6': 'Sonnet 4.6',
|
'claude-sonnet-4-6': 'Sonnet 4.6',
|
||||||
|
|
@ -99,7 +108,8 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
|
||||||
{
|
{
|
||||||
anthropic: [
|
anthropic: [
|
||||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||||
{ value: 'opus', label: 'Opus 4.7', badgeLabel: 'Opus 4.7' },
|
{ value: 'opus', label: 'Opus 4.8', badgeLabel: 'Opus 4.8' },
|
||||||
|
{ value: 'claude-opus-4-7', label: 'Opus 4.7', badgeLabel: 'Opus 4.7' },
|
||||||
{ value: 'claude-opus-4-6', label: 'Opus 4.6', badgeLabel: 'Opus 4.6' },
|
{ value: 'claude-opus-4-6', label: 'Opus 4.6', badgeLabel: 'Opus 4.6' },
|
||||||
{ value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
|
{ value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
|
||||||
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
|
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
|
||||||
|
|
@ -208,6 +218,8 @@ const SUPPORTED_ANTHROPIC_TEAM_MODELS = new Set<string>([
|
||||||
'sonnet',
|
'sonnet',
|
||||||
'sonnet[1m]',
|
'sonnet[1m]',
|
||||||
'haiku',
|
'haiku',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-8[1m]',
|
||||||
'claude-opus-4-7',
|
'claude-opus-4-7',
|
||||||
'claude-opus-4-7[1m]',
|
'claude-opus-4-7[1m]',
|
||||||
'claude-opus-4-6',
|
'claude-opus-4-6',
|
||||||
|
|
@ -397,6 +409,11 @@ export function getRuntimeAwareProviderScopedTeamModelLabel(
|
||||||
model: string | undefined,
|
model: string | undefined,
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
const trimmed = model?.trim();
|
||||||
|
if (providerId === 'anthropic' && (trimmed === 'opus' || trimmed === 'opus[1m]')) {
|
||||||
|
return getProviderScopedTeamModelLabel(providerId, trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
const runtimeLabel = runtimeModel?.displayName?.trim();
|
const runtimeLabel = runtimeModel?.displayName?.trim();
|
||||||
if (runtimeLabel) {
|
if (runtimeLabel) {
|
||||||
|
|
@ -411,6 +428,11 @@ export function getRuntimeAwareTeamModelBadgeLabel(
|
||||||
model: string | undefined,
|
model: string | undefined,
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
const trimmed = model?.trim();
|
||||||
|
if (providerId === 'anthropic' && (trimmed === 'opus' || trimmed === 'opus[1m]')) {
|
||||||
|
return getTeamModelBadgeLabel(providerId, trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
if (runtimeModel?.badgeLabel?.trim()) {
|
if (runtimeModel?.badgeLabel?.trim()) {
|
||||||
return runtimeModel.badgeLabel.trim();
|
return runtimeModel.badgeLabel.trim();
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,12 @@ describe('contextMetrics', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('infers Anthropic native 1M windows from current raw model ids', () => {
|
it('infers Anthropic native 1M windows from current raw model ids', () => {
|
||||||
|
expect(
|
||||||
|
inferContextWindowTokens({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelName: 'claude-opus-4-8',
|
||||||
|
})
|
||||||
|
).toBe(1_000_000);
|
||||||
expect(
|
expect(
|
||||||
inferContextWindowTokens({
|
inferContextWindowTokens({
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ function isAnthropicNativeLongContextModel(modelName: string | undefined): boole
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
normalized.startsWith('claude-opus-4-8') ||
|
||||||
normalized.startsWith('claude-opus-4-7') ||
|
normalized.startsWith('claude-opus-4-7') ||
|
||||||
normalized.startsWith('claude-opus-4-6') ||
|
normalized.startsWith('claude-opus-4-6') ||
|
||||||
normalized.startsWith('claude-sonnet-4-6') ||
|
normalized.startsWith('claude-sonnet-4-6') ||
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,13 @@ describe('formatTeamModelSummary', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats current Anthropic Opus model ids with the latest 4.7 label', () => {
|
it('formats current Anthropic Opus model ids with the latest 4.8 label', () => {
|
||||||
|
expect(formatTeamModelSummary('anthropic', 'claude-opus-4-8', 'high')).toBe(
|
||||||
|
'Anthropic · Opus 4.8 · High'
|
||||||
|
);
|
||||||
|
expect(formatTeamModelSummary('codex', 'claude-opus-4-8', 'medium')).toBe(
|
||||||
|
'Opus 4.8 · via Codex · Medium'
|
||||||
|
);
|
||||||
expect(formatTeamModelSummary('anthropic', 'claude-opus-4-7', 'high')).toBe(
|
expect(formatTeamModelSummary('anthropic', 'claude-opus-4-7', 'high')).toBe(
|
||||||
'Anthropic · Opus 4.7 · High'
|
'Anthropic · Opus 4.7 · High'
|
||||||
);
|
);
|
||||||
|
|
@ -122,6 +128,7 @@ describe('formatTeamModelSummary', () => {
|
||||||
expect(getTeamModelSelectionError('codex', 'gpt-5.4')).toBeNull();
|
expect(getTeamModelSelectionError('codex', 'gpt-5.4')).toBeNull();
|
||||||
expect(getTeamModelSelectionError('codex', '')).toBeNull();
|
expect(getTeamModelSelectionError('codex', '')).toBeNull();
|
||||||
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
||||||
|
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-8')).toBeNull();
|
||||||
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -237,6 +244,7 @@ describe('computeEffectiveTeamModel', () => {
|
||||||
expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus');
|
expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus');
|
||||||
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
|
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
|
||||||
expect(computeEffectiveTeamModel('', true, 'anthropic')).toBe('opus');
|
expect(computeEffectiveTeamModel('', true, 'anthropic')).toBe('opus');
|
||||||
|
expect(computeEffectiveTeamModel('claude-opus-4-8[1m]', true, 'anthropic')).toBe('opus');
|
||||||
expect(computeEffectiveTeamModel('claude-opus-4-7[1m]', true, 'anthropic')).toBe('opus');
|
expect(computeEffectiveTeamModel('claude-opus-4-7[1m]', true, 'anthropic')).toBe('opus');
|
||||||
expect(computeEffectiveTeamModel('claude-sonnet-4-6', true, 'anthropic')).toBe('sonnet');
|
expect(computeEffectiveTeamModel('claude-sonnet-4-6', true, 'anthropic')).toBe('sonnet');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,68 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a temporary New ribbon for Opus 4.8 during the launch window', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(Date.UTC(2026, 4, 31));
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(TeamModelSelector, {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
onProviderChange: () => undefined,
|
||||||
|
value: 'opus',
|
||||||
|
onValueChange: () => undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const opus48Button = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.trim().startsWith('Opus 4.8')
|
||||||
|
);
|
||||||
|
expect(opus48Button?.textContent).toContain('New');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
dateNowSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the Opus 4.8 New ribbon after the launch window expires', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(Date.UTC(2026, 5, 12));
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(TeamModelSelector, {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
onProviderChange: () => undefined,
|
||||||
|
value: 'opus',
|
||||||
|
onValueChange: () => undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const opus48Button = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.trim().startsWith('Opus 4.8')
|
||||||
|
);
|
||||||
|
expect(opus48Button?.textContent).not.toContain('New');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
dateNowSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the runtime-reported Codex list and clears stale unsupported selections', async () => {
|
it('uses the runtime-reported Codex list and clears stale unsupported selections', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliStatus = {
|
storeState.cliStatus = {
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,7 @@ describe('teamModelAvailability', () => {
|
||||||
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps both Anthropic Opus 4.7 and explicit Opus 4.6 in the fallback selector options', () => {
|
it('keeps Anthropic Opus 4.8, explicit 4.7, and explicit 4.6 in the fallback selector options', () => {
|
||||||
expect(getAvailableTeamProviderModelOptions('anthropic')).toEqual([
|
expect(getAvailableTeamProviderModelOptions('anthropic')).toEqual([
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
|
|
@ -555,6 +555,13 @@ describe('teamModelAvailability', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'opus',
|
value: 'opus',
|
||||||
|
label: 'Opus 4.8',
|
||||||
|
badgeLabel: 'Opus 4.8',
|
||||||
|
availabilityStatus: 'available',
|
||||||
|
availabilityReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'claude-opus-4-7',
|
||||||
label: 'Opus 4.7',
|
label: 'Opus 4.7',
|
||||||
badgeLabel: 'Opus 4.7',
|
badgeLabel: 'Opus 4.7',
|
||||||
availabilityStatus: 'available',
|
availabilityStatus: 'available',
|
||||||
|
|
@ -584,12 +591,68 @@ describe('teamModelAvailability', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not let stale first-party Anthropic runtime labels downgrade the Opus alias', () => {
|
||||||
|
const providerStatus: TeamModelRuntimeProviderStatus = {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
models: ['opus', 'claude-opus-4-7'],
|
||||||
|
authMethod: 'oauth',
|
||||||
|
backend: null,
|
||||||
|
authenticated: true,
|
||||||
|
supported: true,
|
||||||
|
modelVerificationState: 'idle',
|
||||||
|
modelAvailability: [],
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-31T00:00:00.000Z',
|
||||||
|
staleAt: '2026-05-31T00:10:00.000Z',
|
||||||
|
defaultModelId: 'opus',
|
||||||
|
defaultLaunchModel: 'opus',
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'ready',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'opus',
|
||||||
|
launchModel: 'opus',
|
||||||
|
displayName: 'Opus 4.7',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||||
|
defaultReasoningEffort: 'high',
|
||||||
|
inputModalities: ['text', 'image'],
|
||||||
|
supportsPersonality: false,
|
||||||
|
isDefault: true,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = getAvailableTeamProviderModelOptions('anthropic', providerStatus);
|
||||||
|
|
||||||
|
expect(options.find((option) => option.value === 'opus')).toMatchObject({
|
||||||
|
label: 'Opus 4.8',
|
||||||
|
badgeLabel: 'Opus 4.8',
|
||||||
|
});
|
||||||
|
expect(options.find((option) => option.value === 'claude-opus-4-7')).toMatchObject({
|
||||||
|
label: 'Opus 4.7',
|
||||||
|
badgeLabel: 'Opus 4.7',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
|
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
|
||||||
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8')).toBe('claude-opus-4-8');
|
||||||
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8[1m]')).toBe('claude-opus-4-8[1m]');
|
||||||
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7')).toBe('claude-opus-4-7');
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7')).toBe('claude-opus-4-7');
|
||||||
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7[1m]')).toBe('claude-opus-4-7[1m]');
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7[1m]')).toBe('claude-opus-4-7[1m]');
|
||||||
expect(normalizeTeamModelForUi('anthropic', 'claude-haiku-4-5-20251001')).toBe(
|
expect(normalizeTeamModelForUi('anthropic', 'claude-haiku-4-5-20251001')).toBe(
|
||||||
'claude-haiku-4-5-20251001'
|
'claude-haiku-4-5-20251001'
|
||||||
);
|
);
|
||||||
|
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-8')).toBeNull();
|
||||||
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
||||||
expect(getTeamModelSelectionError('anthropic', 'claude-haiku-4-5-20251001')).toBeNull();
|
expect(getTeamModelSelectionError('anthropic', 'claude-haiku-4-5-20251001')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ describe('teamModelCatalog', () => {
|
||||||
).toEqual(['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2', 'gpt-5.1-codex-max']);
|
).toEqual(['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2', 'gpt-5.1-codex-max']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds curated Anthropic Opus 4.7 badges when the runtime list only reports legacy Opus variants', () => {
|
it('adds curated Anthropic Opus 4.8 badges when the runtime list only reports legacy Opus variants', () => {
|
||||||
expect(
|
expect(
|
||||||
getVisibleTeamProviderModels('anthropic', [
|
getVisibleTeamProviderModels('anthropic', [
|
||||||
'claude-haiku-4-5-20251001',
|
'claude-haiku-4-5-20251001',
|
||||||
|
|
@ -33,6 +33,8 @@ describe('teamModelCatalog', () => {
|
||||||
])
|
])
|
||||||
).toEqual([
|
).toEqual([
|
||||||
'claude-haiku-4-5-20251001',
|
'claude-haiku-4-5-20251001',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-8[1m]',
|
||||||
'claude-opus-4-7',
|
'claude-opus-4-7',
|
||||||
'claude-opus-4-7[1m]',
|
'claude-opus-4-7[1m]',
|
||||||
'claude-opus-4-6',
|
'claude-opus-4-6',
|
||||||
|
|
@ -301,6 +303,8 @@ describe('teamModelCatalog', () => {
|
||||||
it('detects 1M Anthropic selections and native 1M launch ids', () => {
|
it('detects 1M Anthropic selections and native 1M launch ids', () => {
|
||||||
expect(isAnthropicOneMillionContextTeamModel('sonnet')).toBe(false);
|
expect(isAnthropicOneMillionContextTeamModel('sonnet')).toBe(false);
|
||||||
expect(isAnthropicOneMillionContextTeamModel('sonnet[1m]')).toBe(true);
|
expect(isAnthropicOneMillionContextTeamModel('sonnet[1m]')).toBe(true);
|
||||||
|
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-8')).toBe(true);
|
||||||
|
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-8[1m]')).toBe(true);
|
||||||
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-7')).toBe(true);
|
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-7')).toBe(true);
|
||||||
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-7[1m]')).toBe(true);
|
expect(isAnthropicOneMillionContextTeamModel('claude-opus-4-7[1m]')).toBe(true);
|
||||||
expect(isAnthropicOneMillionContextTeamModel('claude-sonnet-4-6')).toBe(true);
|
expect(isAnthropicOneMillionContextTeamModel('claude-sonnet-4-6')).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getTeamModelRecommendation,
|
getTeamModelRecommendation,
|
||||||
isTeamModelRecommended,
|
isTeamModelRecommended,
|
||||||
} from '@renderer/utils/teamModelRecommendations';
|
} from '@renderer/utils/teamModelRecommendations';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('getTeamModelRecommendation', () => {
|
describe('getTeamModelRecommendation', () => {
|
||||||
it('marks all visible Codex Agent Teams models as recommended', () => {
|
it('marks all visible Codex Agent Teams models as recommended', () => {
|
||||||
|
|
@ -32,6 +31,8 @@ describe('getTeamModelRecommendation', () => {
|
||||||
'claude-haiku-4-5-20251001',
|
'claude-haiku-4-5-20251001',
|
||||||
'claude-sonnet-4-6',
|
'claude-sonnet-4-6',
|
||||||
'claude-sonnet-4-6[1m]',
|
'claude-sonnet-4-6[1m]',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-8[1m]',
|
||||||
'claude-opus-4-7',
|
'claude-opus-4-7',
|
||||||
'claude-opus-4-7[1m]',
|
'claude-opus-4-7[1m]',
|
||||||
'claude-opus-4-6',
|
'claude-opus-4-6',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue