fix(team): support anthropic opus 4.7 team models
This commit is contained in:
parent
aa46d439ce
commit
1a04b49d24
8 changed files with 128 additions and 12 deletions
|
|
@ -55,7 +55,7 @@
|
|||
import Anthropic from '@anthropic-ai/sdk';
|
||||
const client = new Anthropic();
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-opus-4-6',
|
||||
model: 'claude-opus-4-7',
|
||||
messages: [{ role: 'user', content: 'Send message to teammate...' }],
|
||||
tools: [/* SendMessage, TaskUpdate, etc. */]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
getProviderScopedTeamModelLabel,
|
||||
getTeamModelLabel as getCatalogTeamModelLabel,
|
||||
getTeamProviderLabel as getCatalogTeamProviderLabel,
|
||||
isAnthropicHaikuTeamModel,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
|
||||
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
|
||||
|
|
@ -109,7 +110,7 @@ export function computeEffectiveTeamModel(
|
|||
|
||||
const base = extractProviderScopedBaseModel(selectedModel, providerId);
|
||||
if (limitContext) return base || getAnthropicDefaultTeamModel(true);
|
||||
if (base === 'haiku') return base;
|
||||
if (isAnthropicHaikuTeamModel(base)) return base;
|
||||
return base ? `${base}[1m]` : getAnthropicDefaultTeamModel(limitContext);
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +142,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
|
||||
const defaultModelTooltip = useMemo(() => {
|
||||
if (effectiveProviderId === 'anthropic') {
|
||||
return 'Uses the Claude team default model.\nResolves to Opus 1M, or Opus 200K when Limit context is enabled.';
|
||||
return 'Uses the Claude team default model.\nResolves to Opus 4.7 with 1M context, or Opus 4.7 with 200K context when Limit context is enabled.';
|
||||
}
|
||||
return 'Uses the runtime default for the selected provider.';
|
||||
}, [effectiveProviderId]);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Label } from '@renderer/components/ui/label';
|
|||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog';
|
||||
import { getMemberColorByName } from '@shared/constants/memberColors';
|
||||
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
|
||||
|
||||
|
|
@ -151,7 +152,7 @@ export const LeadModelRow = ({
|
|||
id="lead-limit-context"
|
||||
checked={limitContext}
|
||||
onCheckedChange={onLimitContextChange}
|
||||
disabled={model === 'haiku'}
|
||||
disabled={isAnthropicHaikuTeamModel(model)}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex items-start gap-2 rounded-md border border-sky-500/20 bg-sky-500/5 px-3 py-2">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
getProviderScopedTeamModelLabel,
|
||||
isSupportedAnthropicTeamModel,
|
||||
getRuntimeAwareTeamModelUiDisabledReason,
|
||||
getTeamProviderLabel,
|
||||
getTeamProviderModelOptions,
|
||||
|
|
@ -230,7 +231,7 @@ export function isTeamModelAvailableForUi(
|
|||
}
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
return getFallbackTeamProviderModels(providerId).includes(trimmed);
|
||||
return isSupportedAnthropicTeamModel(trimmed);
|
||||
}
|
||||
|
||||
return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
GPT_5_2_CODEX_UI_DISABLED_MODEL,
|
||||
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
|
||||
} from '@shared/utils/providerModelVisibility';
|
||||
import { parseModelString } from '@shared/utils/modelParser';
|
||||
|
||||
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
|
||||
|
||||
|
|
@ -39,15 +40,22 @@ const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
|
|||
gemini: 'Gemini',
|
||||
};
|
||||
|
||||
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
default: 'Default',
|
||||
opus: 'Opus 4.6',
|
||||
const ANTHROPIC_ALIAS_LABELS = {
|
||||
opus: 'Opus 4.7',
|
||||
sonnet: 'Sonnet 4.6',
|
||||
haiku: 'Haiku 4.5',
|
||||
} as const;
|
||||
|
||||
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
default: 'Default',
|
||||
...ANTHROPIC_ALIAS_LABELS,
|
||||
'claude-opus-4-7': 'Opus 4.7',
|
||||
'claude-opus-4-7[1m]': 'Opus 4.7 (1M)',
|
||||
'claude-sonnet-4-6': 'Sonnet 4.6',
|
||||
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
|
||||
'claude-opus-4-6': 'Opus 4.6',
|
||||
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
|
||||
'claude-haiku-4-5': 'Haiku 4.5',
|
||||
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
'gpt-5.4-mini': 'GPT-5.4 Mini',
|
||||
|
|
@ -66,7 +74,7 @@ const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProv
|
|||
{
|
||||
anthropic: [
|
||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||
{ value: 'opus', label: 'Opus 4.6', badgeLabel: 'Opus 4.6' },
|
||||
{ value: 'opus', label: 'Opus 4.7', badgeLabel: 'Opus 4.7' },
|
||||
{ value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
|
||||
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
|
||||
],
|
||||
|
|
@ -133,6 +141,73 @@ export function getTeamProviderModelOptions(
|
|||
return TEAM_PROVIDER_MODEL_OPTIONS[providerId];
|
||||
}
|
||||
|
||||
function splitOneMillionContextSuffix(model: string): {
|
||||
baseModel: string;
|
||||
hasOneMillion: boolean;
|
||||
} {
|
||||
const hasOneMillion = /\[1m\]$/i.test(model);
|
||||
return {
|
||||
baseModel: model.replace(/\[1m\]$/i, ''),
|
||||
hasOneMillion,
|
||||
};
|
||||
}
|
||||
|
||||
function formatParsedClaudeModelLabel(model: string): string | null {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { baseModel, hasOneMillion } = splitOneMillionContextSuffix(trimmed);
|
||||
const parsedModel = parseModelString(baseModel);
|
||||
if (!parsedModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const familyLabel = parsedModel.family.charAt(0).toUpperCase() + parsedModel.family.slice(1);
|
||||
const versionLabel =
|
||||
parsedModel.minorVersion == null
|
||||
? `${parsedModel.majorVersion}`
|
||||
: `${parsedModel.majorVersion}.${parsedModel.minorVersion}`;
|
||||
|
||||
return `${familyLabel} ${versionLabel}${hasOneMillion ? ' (1M)' : ''}`;
|
||||
}
|
||||
|
||||
const SUPPORTED_ANTHROPIC_TEAM_MODELS = new Set<string>([
|
||||
'opus',
|
||||
'opus[1m]',
|
||||
'sonnet',
|
||||
'sonnet[1m]',
|
||||
'haiku',
|
||||
'claude-opus-4-7',
|
||||
'claude-opus-4-7[1m]',
|
||||
'claude-opus-4-6',
|
||||
'claude-opus-4-6[1m]',
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6[1m]',
|
||||
'claude-haiku-4-5',
|
||||
'claude-haiku-4-5-20251001',
|
||||
]);
|
||||
|
||||
export function isSupportedAnthropicTeamModel(model: string | undefined): boolean {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SUPPORTED_ANTHROPIC_TEAM_MODELS.has(trimmed);
|
||||
}
|
||||
|
||||
export function isAnthropicHaikuTeamModel(model: string | undefined): boolean {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { baseModel } = splitOneMillionContextSuffix(trimmed);
|
||||
return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-');
|
||||
}
|
||||
|
||||
export function getTeamProviderLabel(
|
||||
providerId: SupportedProviderId | undefined
|
||||
): string | undefined {
|
||||
|
|
@ -147,7 +222,13 @@ export function getTeamModelLabel(model: string | undefined): string | undefined
|
|||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return TEAM_MODEL_LABEL_OVERRIDES[trimmed] ?? trimmed;
|
||||
|
||||
const overrideLabel = TEAM_MODEL_LABEL_OVERRIDES[trimmed];
|
||||
if (overrideLabel) {
|
||||
return overrideLabel;
|
||||
}
|
||||
|
||||
return formatParsedClaudeModelLabel(trimmed) ?? trimmed;
|
||||
}
|
||||
|
||||
export function getTeamModelBadgeLabel(
|
||||
|
|
@ -165,6 +246,10 @@ export function getTeamModelBadgeLabel(
|
|||
}
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
const anthropicLabel = getTeamModelLabel(trimmed);
|
||||
if (anthropicLabel && anthropicLabel !== trimmed) {
|
||||
return anthropicLabel;
|
||||
}
|
||||
return trimmed.replace(/^claude-/, '');
|
||||
}
|
||||
if (providerId === 'codex') {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ describe('formatTeamModelSummary', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('formats current Anthropic Opus model ids with the latest 4.7 label', () => {
|
||||
expect(formatTeamModelSummary('anthropic', 'claude-opus-4-7', 'high')).toBe(
|
||||
'Anthropic · Opus 4.7 · High'
|
||||
);
|
||||
expect(formatTeamModelSummary('codex', 'claude-opus-4-7', 'medium')).toBe(
|
||||
'Opus 4.7 · via Codex · Medium'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps native Codex-family models branded normally', () => {
|
||||
expect(formatTeamModelSummary('codex', 'gpt-5.4', 'medium')).toBe('5.4 · Medium');
|
||||
});
|
||||
|
|
@ -106,6 +115,7 @@ describe('formatTeamModelSummary', () => {
|
|||
expect(getTeamModelSelectionError('codex', 'gpt-5.4')).toContain('waiting for Codex runtime verification');
|
||||
expect(getTeamModelSelectionError('codex', '')).toBeNull();
|
||||
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
||||
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -130,10 +140,16 @@ describe('computeEffectiveTeamModel', () => {
|
|||
expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus');
|
||||
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
|
||||
expect(computeEffectiveTeamModel('', true, 'anthropic')).toBe('opus');
|
||||
expect(computeEffectiveTeamModel('claude-opus-4-7[1m]', true, 'anthropic')).toBe(
|
||||
'claude-opus-4-7'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns haiku as-is', () => {
|
||||
expect(computeEffectiveTeamModel('haiku', false, 'anthropic')).toBe('haiku');
|
||||
expect(computeEffectiveTeamModel('claude-haiku-4-5-20251001', false, 'anthropic')).toBe(
|
||||
'claude-haiku-4-5-20251001'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns non-anthropic models as-is', () => {
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ function createSpawnEntry(overrides: Partial<MemberSpawnStatusEntry> = {}): Memb
|
|||
describe('resolveMemberRuntimeSummary', () => {
|
||||
it('shows the live runtime model for loading members when available', () => {
|
||||
const member = createMember();
|
||||
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-6', runtimeAlive: true });
|
||||
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
|
||||
|
||||
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
|
||||
'Anthropic · Opus 4.6 · Medium'
|
||||
'Anthropic · Opus 4.7 · Medium'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -116,4 +116,16 @@ describe('teamModelAvailability', () => {
|
|||
expect(normalizeTeamModelForUi('anthropic', 'opus')).toBe('opus');
|
||||
expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
|
||||
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-haiku-4-5-20251001')).toBe(
|
||||
'claude-haiku-4-5-20251001'
|
||||
);
|
||||
expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull();
|
||||
expect(getTeamModelSelectionError('anthropic', 'claude-haiku-4-5-20251001')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue