diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f8e10cc0..c2c85f23 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1041,6 +1041,7 @@ interface AuthStatusCommandResponse { interface RuntimeProviderLaunchFacts { defaultModel: string | null; modelIds: Set; + modelListParsed?: boolean; modelCatalog: CliProviderModelCatalog | null; runtimeCapabilities: CliProviderRuntimeCapabilities | null; providerStatus?: @@ -1141,6 +1142,20 @@ function isCodexEffortRuntimeSupported( return reasoning?.configPassthrough === true && reasoning.values.includes(effort); } +function hasAuthoritativeCodexLaunchCatalog( + facts: Pick< + RuntimeProviderLaunchFacts, + 'modelIds' | 'modelListParsed' | 'modelCatalog' | 'runtimeCapabilities' + > +): boolean { + if (facts.modelIds.size > 0 || facts.modelCatalog != null) { + return true; + } + return ( + facts.modelListParsed === true && facts.runtimeCapabilities?.modelCatalog?.dynamic === false + ); +} + function getAnthropicFastModeDefault(): boolean { return ( ConfigManager.getInstance().getConfig().providerConnections.anthropic.fastModeDefault === true @@ -6117,11 +6132,13 @@ export class TeamProvisioningService { let defaultModel: string | null = null; let modelIds = new Set(); + let modelListParsed = false; if (modelListResult.status === 'fulfilled') { try { const parsed = extractJsonObjectFromCli( modelListResult.value.stdout ); + modelListParsed = true; const provider = parsed.providers?.[params.providerId]; defaultModel = typeof provider?.defaultModel === 'string' && provider.defaultModel.trim().length > 0 @@ -6202,6 +6219,7 @@ export class TeamProvisioningService { }) : defaultModel, modelIds, + modelListParsed, modelCatalog, runtimeCapabilities, providerStatus, @@ -6400,6 +6418,10 @@ export class TeamProvisioningService { return; } + if (!hasAuthoritativeCodexLaunchCatalog(params.facts)) { + return; + } + throw new Error( `${params.actorLabel} uses Codex model "${explicitModel}", but this Agent Teams runtime does not declare dynamic Codex model launch support yet. Upgrade the runtime or pick a listed Codex model.` ); @@ -17152,9 +17174,18 @@ export class TeamProvisioningService { const dynamicCatalog = params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === true; const hasAuthoritativeCatalog = - availableModels.size > 0 || - params.runtimeFacts.modelCatalog != null || - params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false; + params.providerId === 'codex' + ? hasAuthoritativeCodexLaunchCatalog(params.runtimeFacts) + : availableModels.size > 0 || + params.runtimeFacts.modelCatalog != null || + params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false; + + if (params.providerId === 'codex' && (dynamicCatalog || !hasAuthoritativeCatalog)) { + return { + kind: 'available', + resolvedModelId: trimmedModelId, + }; + } if (dynamicCatalog || !hasAuthoritativeCatalog) { return { diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx index dd4ff008..20f570d5 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx @@ -91,7 +91,9 @@ export function getProvisioningProviderBackendSummary( suffixes.push('runtime missing'); break; case 'degraded': - suffixes.push('degraded'); + if (inferredProviderId !== 'codex') { + suffixes.push('degraded'); + } break; default: break; diff --git a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts index e182d279..f627372f 100644 --- a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts +++ b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts @@ -311,6 +311,7 @@ function isSuppressibleGenericPreflightWarning(detail: string): boolean { } return ( + lower.includes('one-shot diagnostic') || lower.includes('preflight check failed') || (lower.includes('preflight check for `') && lower.includes('-p` did not complete')) || lower.includes('preflight ping completed but did not return the expected pong') @@ -413,6 +414,13 @@ function resolveModelResultFromBatch( /selected model .* is compatible\. deep verification pending\./i.test(entry) ); if (hasCompatibilityLine) { + if (providerId !== 'opencode') { + return { + status: 'ready', + line: buildModelAvailableLine(providerId, modelId), + warningLine: null, + }; + } return { status: 'notes', line: buildModelCompatibilityPendingLine(providerId, modelId), @@ -452,6 +460,13 @@ function resolveModelResultFromBatch( } if (result.ready && (result.warnings?.length ?? 0) > 0 && !hasModelScopedEntries) { + if (providerId !== 'opencode') { + return { + status: 'ready', + line: buildModelAvailableLine(providerId, modelId), + warningLine: null, + }; + } return { status: 'notes', line: buildModelCompatibilityPendingLine(providerId, modelId), diff --git a/src/renderer/components/team/members/LeadModelRow.test.tsx b/src/renderer/components/team/members/LeadModelRow.test.tsx index 24a40296..e9686163 100644 --- a/src/renderer/components/team/members/LeadModelRow.test.tsx +++ b/src/renderer/components/team/members/LeadModelRow.test.tsx @@ -55,6 +55,8 @@ vi.mock('@renderer/hooks/useTheme', () => ({ vi.mock('@renderer/utils/teamModelCatalog', () => ({ isAnthropicHaikuTeamModel: () => false, + isAnthropicSonnetTeamModel: (model: string | undefined) => + model === 'sonnet' || model === 'claude-sonnet-4-6' || model === 'sonnet[1m]', })); vi.mock('../../ui/button', () => ({ @@ -78,9 +80,12 @@ vi.mock('../../ui/button', () => ({ ), })); -import { LeadModelRow } from './LeadModelRow'; +import { ANTHROPIC_LONG_CONTEXT_PRICING_URL, LeadModelRow } from './LeadModelRow'; -function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType } { +function renderLeadModelRow(overrides: Partial> = {}): { + host: HTMLDivElement; + root: ReturnType; +} { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -98,6 +103,7 @@ function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType undefined, syncModelsWithTeammates: true, onSyncModelsWithTeammatesChange: () => undefined, + ...overrides, }) ); }); @@ -128,4 +134,38 @@ describe('LeadModelRow', () => { root.unmount(); }); }); + + it('warns that unchecked 200K limit can put Sonnet on Anthropic Extra Usage', () => { + const { host, root } = renderLeadModelRow({ + providerId: 'anthropic', + model: 'sonnet', + limitContext: false, + }); + + expect(host.textContent).toContain('Sonnet with 1M context can use Anthropic Extra Usage'); + expect(host.textContent).toContain('Requests over 200K input tokens'); + const docsLink = host.querySelector(`a[href="${ANTHROPIC_LONG_CONTEXT_PRICING_URL}"]`); + + expect(docsLink?.textContent).toContain('Anthropic pricing docs'); + expect(docsLink?.getAttribute('target')).toBe('_blank'); + expect(docsLink?.getAttribute('rel')).toBe('noreferrer'); + + act(() => { + root.unmount(); + }); + }); + + it('does not show the Sonnet Extra Usage warning when 200K limit is enabled', () => { + const { host, root } = renderLeadModelRow({ + providerId: 'anthropic', + model: 'sonnet', + limitContext: true, + }); + + expect(host.textContent).not.toContain('Anthropic Extra Usage'); + + act(() => { + root.unmount(); + }); + }); }); diff --git a/src/renderer/components/team/members/LeadModelRow.tsx b/src/renderer/components/team/members/LeadModelRow.tsx index 7b75b9f5..b1dbb506 100644 --- a/src/renderer/components/team/members/LeadModelRow.tsx +++ b/src/renderer/components/team/members/LeadModelRow.tsx @@ -14,7 +14,10 @@ import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { agentAvatarUrl } from '@renderer/utils/memberHelpers'; -import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog'; +import { + isAnthropicHaikuTeamModel, + isAnthropicSonnetTeamModel, +} from '@renderer/utils/teamModelCatalog'; import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors'; import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react'; @@ -22,6 +25,11 @@ import { Button } from '../../ui/button'; import type { EffortLevel, TeamProviderId } from '@shared/types'; +export const ANTHROPIC_SONNET_EXTRA_USAGE_WARNING = + 'Sonnet with 1M context can use Anthropic Extra Usage. Requests over 200K input tokens are billed at premium long-context rates; enable Limit context to 200K tokens to avoid that billing path.'; +export const ANTHROPIC_LONG_CONTEXT_PRICING_URL = + 'https://platform.claude.com/docs/en/about-claude/pricing'; + interface LeadModelRowProps { providerId: TeamProviderId; model: string; @@ -61,6 +69,12 @@ export const LeadModelRow = ({ : 'Default'; const modelButtonAriaLabel = `${getTeamProviderLabel(providerId)} provider, ${modelButtonLabel}`; const hasModelIssue = Boolean(modelIssueText); + const showSonnetExtraUsageWarning = + providerId === 'anthropic' && !limitContext && isAnthropicSonnetTeamModel(model); + const warningMessages = [warningText?.trim() || null].filter((message): message is string => + Boolean(message) + ); + const hasWarnings = warningMessages.length > 0 || showSonnetExtraUsageWarning; return (
- {warningText ? ( + {hasWarnings ? (
-

{warningText}

+
+ {warningMessages.map((message) => ( +

{message}

+ ))} + {showSonnetExtraUsageWarning ? ( +

+ {ANTHROPIC_SONNET_EXTRA_USAGE_WARNING}{' '} + + Read Anthropic pricing docs + + . +

+ ) : null} +
) : null} diff --git a/src/renderer/index.html b/src/renderer/index.html index 6006ef3c..63c01a9e 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -250,10 +250,10 @@ width: 100%; align-items: baseline; justify-content: center; - gap: 6px; + gap: 4px; } #splash-status { - min-height: 16px; + min-height: 8px; font-family: ui-sans-serif, system-ui, @@ -261,21 +261,21 @@ BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-size: 12px; + font-size: 8px; font-weight: 500; color: rgba(212, 212, 216, 0.72); - line-height: 1.35; + line-height: 1.25; overflow-wrap: anywhere; } #splash-elapsed::before { content: 'ยท'; - margin-right: 6px; + margin-right: 4px; color: rgba(212, 212, 216, 0.34); } #splash-elapsed { font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - font-size: 11px; + font-size: 8px; font-weight: 500; color: rgba(212, 212, 216, 0.5); white-space: nowrap; @@ -295,14 +295,18 @@ line-height: 1.35; } #splash-timeline { - display: flex; + display: none; width: min(320px, 78vw); - max-height: 128px; - margin-top: 14px; + max-height: 58px; + margin-top: 8px; flex-direction: column; gap: 6px; overflow: hidden; } + #splash.splash-status-error #splash-timeline, + #splash.splash-status-slow #splash-timeline { + display: flex; + } .splash-step { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; @@ -494,6 +498,18 @@ (function () { try { window.__claudeTeamsSplashStartedAt = performance.now(); + window.__claudeTeamsSplashStaticTimer = window.setInterval(function () { + var elapsed = document.getElementById('splash-elapsed'); + var splash = document.getElementById('splash'); + if (!elapsed || !splash) { + window.clearInterval(window.__claudeTeamsSplashStaticTimer); + window.__claudeTeamsSplashStaticTimer = undefined; + return; + } + var startedAt = window.__claudeTeamsSplashStartedAt || performance.now(); + var seconds = Math.max(0, Math.floor((performance.now() - startedAt) / 1000)); + elapsed.textContent = seconds + 's'; + }, 1000); var theme = localStorage.getItem('agent-teams-theme-cache') || localStorage.getItem('claude-devtools-theme-cache'); diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 2dd7f54e..b49c1db9 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -14,6 +14,7 @@ import type { AppStartupStatus, AppStartupStep } from '@shared/types/api'; declare global { interface Window { __claudeTeamsUiDidInit?: boolean; + __claudeTeamsSplashStaticTimer?: number; } } @@ -26,7 +27,7 @@ let startupTicker: number | undefined; const SLOW_STEP_MS = 7_000; const VERY_SLOW_STEP_MS = 14_000; -const TIMELINE_STEP_LIMIT = 6; +const TIMELINE_STEP_LIMIT = 3; function getStartupErrorText(status: AppStartupStatus): string { return status.error ? `Startup failed: ${status.error}` : 'Startup failed. Please restart.'; @@ -142,6 +143,12 @@ function updateStartupSplash(status: AppStartupStatus): void { renderStartupTimeline(status); } +function stopStaticSplashTimer(): void { + if (window.__claudeTeamsSplashStaticTimer === undefined) return; + window.clearInterval(window.__claudeTeamsSplashStaticTimer); + window.__claudeTeamsSplashStaticTimer = undefined; +} + function startStartupTicker(): void { if (startupTicker !== undefined) return; startupTicker = window.setInterval(() => { @@ -194,13 +201,16 @@ async function bootstrapRenderer(): Promise { if (nextStatus.ready) { finished = true; cleanup(); + stopStaticSplashTimer(); stopStartupTicker(); mountApp(); } else if (nextStatus.error) { finished = true; cleanup(); + stopStaticSplashTimer(); stopStartupTicker(); } else { + stopStaticSplashTimer(); startStartupTicker(); } }; @@ -210,6 +220,7 @@ async function bootstrapRenderer(): Promise { } catch (error) { console.warn(`[startup] status bridge unavailable: ${String(error)}`); cleanup(); + stopStaticSplashTimer(); stopStartupTicker(); mountApp(); } diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index eb31e052..f245d3d7 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -239,6 +239,16 @@ export function isAnthropicHaikuTeamModel(model: string | undefined): boolean { return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-'); } +export function isAnthropicSonnetTeamModel(model: string | undefined): boolean { + const trimmed = model?.trim(); + if (!trimmed) { + return false; + } + + const { baseModel } = splitOneMillionContextSuffix(trimmed); + return baseModel === 'sonnet' || baseModel.startsWith('claude-sonnet-'); +} + export function getTeamProviderLabel( providerId: SupportedProviderId | undefined ): string | undefined { diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index cfd0fb06..758e0c40 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -2005,7 +2005,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { ); }); - it('keeps missing models compatible when the runtime catalog is dynamic', async () => { + it('treats missing Codex models as launchable when the runtime catalog is dynamic', async () => { const svc = new TeamProvisioningService(); vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({ env: { @@ -2033,13 +2033,77 @@ describe('TeamProvisioningService prepare/auth behavior', () => { }); expect(result).toEqual({ - details: ['Selected model future-model is compatible. Deep verification pending.'], + details: ['Selected model future-model is available for launch.'], warnings: [], blockingMessages: [], }); expect(spawnProbe).not.toHaveBeenCalled(); }); + it('treats explicit Codex models as launchable when the runtime model list is unparsable', async () => { + execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => { + if (args[0] === 'model' && args[1] === 'list' && args.includes('codex')) { + return { + stdout: 'Codex model list is temporarily unavailable', + stderr: '', + exitCode: 0, + }; + } + if (args[0] === 'runtime' && args[1] === 'status' && args.includes('codex')) { + return { + stdout: JSON.stringify({ + providers: { + codex: { + runtimeCapabilities: { + modelCatalog: { dynamic: false, source: 'runtime' }, + reasoningEffort: { + supported: true, + values: ['low', 'medium', 'high'], + configPassthrough: false, + }, + }, + }, + }, + }), + stderr: '', + exitCode: 0, + }; + } + return defaultExecCliMockImplementation(_binaryPath, args); + }); + + const svc = new TeamProvisioningService(); + vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({ + claudePath: '/fake/claude', + authSource: 'codex_runtime', + }); + vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({ + env: { + PATH: '/usr/bin', + SHELL: '/bin/zsh', + }, + authSource: 'codex_runtime', + geminiRuntimeAuth: null, + }); + const spawnProbe = vi.spyOn(svc as any, 'spawnProbe'); + + const result = await svc.prepareForProvisioning(tempRoot, { + forceFresh: true, + providerId: 'codex', + modelIds: ['gpt-5.5'], + modelVerificationMode: 'compatibility', + }); + + expect(result.ready).toBe(true); + expect(result.details).toEqual(['Selected model gpt-5.5 is available for launch.']); + expect(result.message).toBe('CLI is warmed up and ready to launch'); + expect(spawnProbe).not.toHaveBeenCalled(); + expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([ + '[Service:TeamProvisioning] [codex] Failed to parse runtime model list for launch validation: No JSON object found in CLI output', + ]); + vi.mocked(console.warn).mockClear(); + }); + it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => { const svc = new TeamProvisioningService(); vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({ diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 1b8356a3..145ddf54 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -469,6 +469,45 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(spawnCli).not.toHaveBeenCalled(); }); + it('allows explicit Codex models when launch model list parsing is degraded', async () => { + vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex'); + const { child } = createFakeChild(); + vi.mocked(spawnCli).mockReturnValue(child as any); + + const svc = new TeamProvisioningService(); + (svc as any).buildProvisioningEnv = vi.fn(async () => ({ + env: {}, + authSource: 'codex_runtime', + providerArgs: [], + })); + (svc as any).readRuntimeProviderLaunchFacts = vi.fn(async () => ({ + defaultModel: null, + modelIds: new Set(), + modelListParsed: false, + modelCatalog: null, + runtimeCapabilities: { modelCatalog: { dynamic: false, source: 'runtime' } }, + providerStatus: null, + })); + (svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {}); + (svc as any).startFilesystemMonitor = vi.fn(); + (svc as any).pathExists = vi.fn(async () => false); + + const { runId } = await svc.createTeam( + { + teamName: 'codex-future-model-launchable', + cwd: process.cwd(), + members: [], + providerId: 'codex', + model: 'gpt-5.5', + effort: 'medium', + }, + () => {} + ); + + expect(spawnCli).toHaveBeenCalled(); + await svc.cancelProvisioning(runId); + }); + it('restart teammate message keeps the exact teammate identity and avoids duplicate semantics', () => { const message = buildRestartMemberSpawnMessage('forge-labs', 'Forge Labs', 'lead', { name: 'alice', diff --git a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts index 5f82f2cd..7ff82ac5 100644 --- a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts +++ b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts @@ -266,6 +266,33 @@ describe('ProvisioningProviderStatusList', () => { ).toBe('Codex native'); }); + it('does not show non-blocking Codex degraded backend state in provisioning summaries', () => { + expect( + getProvisioningProviderBackendSummary({ + providerId: 'codex', + selectedBackendId: 'codex-native', + resolvedBackendId: 'codex-native', + backend: { + kind: 'codex-native', + label: 'Codex native', + }, + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: false, + recommended: false, + available: true, + state: 'degraded', + audience: 'general', + statusMessage: 'Ready with degraded account verification.', + }, + ], + }) + ).toBe('Codex native'); + }); + it('normalizes persisted legacy codex fallback summaries to Codex native', () => { expect( getProvisioningProviderBackendSummary({ diff --git a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts index 46a183da..38e62b5a 100644 --- a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts +++ b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts @@ -570,7 +570,7 @@ describe('runProviderPrepareDiagnostics', () => { expect(result.details).toEqual(['5.4 Mini - verified', '5.4 - verified']); }); - it('does not synthesize verified from a generic runtime preflight note alone', async () => { + it('treats launchable Codex compatibility as ready and suppresses generic preflight notes', async () => { const prepareProvisioning = vi.fn< ( cwd?: string, @@ -593,16 +593,13 @@ describe('runProviderPrepareDiagnostics', () => { prepareProvisioning, }); - expect(result.status).toBe('notes'); - expect(result.warnings).toEqual(['orchestrator-cli preflight check failed (exit code 1).']); - expect(result.details).toEqual([ - 'orchestrator-cli preflight check failed (exit code 1).', - '5.4 - compatible, deep verification pending...', - ]); + expect(result.status).toBe('ready'); + expect(result.warnings).toEqual([]); + expect(result.details).toEqual(['5.4 - available for launch']); expect(result.modelResultsById).toEqual({ 'gpt-5.4': { - status: 'notes', - line: '5.4 - compatible, deep verification pending...', + status: 'ready', + line: '5.4 - available for launch', warningLine: null, }, }); diff --git a/test/renderer/utils/teamModelCatalog.test.ts b/test/renderer/utils/teamModelCatalog.test.ts index 8e63878a..708efdbc 100644 --- a/test/renderer/utils/teamModelCatalog.test.ts +++ b/test/renderer/utils/teamModelCatalog.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog'; +import { + getVisibleTeamProviderModels, + isAnthropicSonnetTeamModel, +} from '@renderer/utils/teamModelCatalog'; describe('teamModelCatalog', () => { it('filters UI-disabled Codex models from provider badge lists', () => { @@ -43,4 +46,13 @@ describe('teamModelCatalog', () => { 'claude-sonnet-4-6[1m]', ]); }); + + it('detects Sonnet aliases with or without 1M suffix', () => { + expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true); + expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true); + expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6')).toBe(true); + expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6[1m]')).toBe(true); + expect(isAnthropicSonnetTeamModel('opus')).toBe(false); + expect(isAnthropicSonnetTeamModel('haiku')).toBe(false); + }); });