From 1a04b49d24a2ccf14a96a65bd5ddad4bc1034866 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 18 Apr 2026 12:09:26 +0300 Subject: [PATCH] fix(team): support anthropic opus 4.7 team models --- docs/team-management/research-messaging.md | 2 +- .../team/dialogs/TeamModelSelector.tsx | 5 +- .../components/team/members/LeadModelRow.tsx | 3 +- src/renderer/utils/teamModelAvailability.ts | 3 +- src/renderer/utils/teamModelCatalog.ts | 95 ++++++++++++++++++- .../components/team/TeamModelSelector.test.ts | 16 ++++ .../utils/memberRuntimeSummary.test.ts | 4 +- .../utils/teamModelAvailability.test.ts | 12 +++ 8 files changed, 128 insertions(+), 12 deletions(-) diff --git a/docs/team-management/research-messaging.md b/docs/team-management/research-messaging.md index 02388c1a..ea5789e1 100644 --- a/docs/team-management/research-messaging.md +++ b/docs/team-management/research-messaging.md @@ -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. */] }); diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index 08b44e1e..55fae00d 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -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 = ({ 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]); diff --git a/src/renderer/components/team/members/LeadModelRow.tsx b/src/renderer/components/team/members/LeadModelRow.tsx index dfa4c323..9039a996 100644 --- a/src/renderer/components/team/members/LeadModelRow.tsx +++ b/src/renderer/components/team/members/LeadModelRow.tsx @@ -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}
diff --git a/src/renderer/utils/teamModelAvailability.ts b/src/renderer/utils/teamModelAvailability.ts index d588a1bc..7fd76aba 100644 --- a/src/renderer/utils/teamModelAvailability.ts +++ b/src/renderer/utils/teamModelAvailability.ts @@ -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'; diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index e53172d8..4e4bc8d8 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -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 = { gemini: 'Gemini', }; -const TEAM_MODEL_LABEL_OVERRIDES: Record = { - 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 = { + 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([ + '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') { diff --git a/test/renderer/components/team/TeamModelSelector.test.ts b/test/renderer/components/team/TeamModelSelector.test.ts index 9762a168..d49ac1c9 100644 --- a/test/renderer/components/team/TeamModelSelector.test.ts +++ b/test/renderer/components/team/TeamModelSelector.test.ts @@ -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', () => { diff --git a/test/renderer/utils/memberRuntimeSummary.test.ts b/test/renderer/utils/memberRuntimeSummary.test.ts index dd8ab9b5..ebf5ecdc 100644 --- a/test/renderer/utils/memberRuntimeSummary.test.ts +++ b/test/renderer/utils/memberRuntimeSummary.test.ts @@ -38,10 +38,10 @@ function createSpawnEntry(overrides: Partial = {}): 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' ); }); diff --git a/test/renderer/utils/teamModelAvailability.test.ts b/test/renderer/utils/teamModelAvailability.test.ts index d9f0a21e..33bedc93 100644 --- a/test/renderer/utils/teamModelAvailability.test.ts +++ b/test/renderer/utils/teamModelAvailability.test.ts @@ -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(); + }); });