From 62a1b0e865fee03956423f57fa5dfb4ac8d57626 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 30 Apr 2026 22:21:29 +0300 Subject: [PATCH] fix: show effective Anthropic API key status --- .../runtime/ProviderConnectionService.ts | 30 ++++++ .../runtime/ProviderRuntimeSettingsDialog.tsx | 32 +++--- .../runtime/providerConnectionUi.ts | 28 +++++ .../runtime/ProviderConnectionService.test.ts | 101 ++++++++++++++++++ .../ProviderRuntimeSettingsDialog.test.ts | 48 +++++++++ .../runtime/providerConnectionUi.test.ts | 30 ++++++ 6 files changed, 253 insertions(+), 16 deletions(-) diff --git a/src/main/services/runtime/ProviderConnectionService.ts b/src/main/services/runtime/ProviderConnectionService.ts index de23f11c..2d7bf80f 100644 --- a/src/main/services/runtime/ProviderConnectionService.ts +++ b/src/main/services/runtime/ProviderConnectionService.ts @@ -423,6 +423,10 @@ export class ProviderConnectionService { connection: await this.getConnectionInfo(provider.providerId), }; + if (provider.providerId === 'anthropic') { + return this.enrichAnthropicProviderStatus(withConnection); + } + if (provider.providerId !== 'codex') { return withConnection; } @@ -480,6 +484,32 @@ export class ProviderConnectionService { } } + private enrichAnthropicProviderStatus(provider: CliProviderStatus): CliProviderStatus { + const connection = provider.connection; + if (connection?.configuredAuthMode !== 'api_key') { + return provider; + } + + if (connection.apiKeyConfigured) { + return { + ...provider, + authenticated: true, + authMethod: 'api_key', + verificationState: + provider.verificationState === 'error' ? provider.verificationState : 'verified', + statusMessage: 'Connected via API key', + }; + } + + return { + ...provider, + authenticated: false, + authMethod: null, + verificationState: provider.verificationState === 'error' ? 'error' : 'unknown', + statusMessage: 'API key mode is selected, but no Anthropic API credential is available yet.', + }; + } + async enrichProviderStatuses(providers: CliProviderStatus[]): Promise { return Promise.all(providers.map((provider) => this.enrichProviderStatus(provider))); } diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index 8e6db338..108e2155 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -265,6 +265,19 @@ function getConnectionAlert(provider: CliProviderStatus): string | null { return null; } +function getProviderUsageLabel(provider: CliProviderStatus): string { + if ( + provider.providerId === 'anthropic' && + provider.connection?.configuredAuthMode === 'api_key' + ) { + return provider.connection.apiKeyConfigured ? 'Using API key' : 'API key required'; + } + + return provider.authenticated + ? `Using ${formatProviderAuthMethodLabelForProvider(provider.providerId, provider.authMethod)}` + : provider.statusMessage || 'Not connected'; +} + function getCodexAccountPanelHint( provider: CliProviderStatus | null, configuredAuthMode: CliProviderAuthMode | undefined @@ -818,10 +831,7 @@ export const ProviderRuntimeSettingsDialog = ({ let connectionStatusLabel: string | null = null; if (selectedProvider) { if (!hideConnectionMethodMeta && selectedProvider.authenticated) { - connectionStatusLabel = `Using ${formatProviderAuthMethodLabelForProvider( - selectedProvider.providerId, - selectedProvider.authMethod - )}`; + connectionStatusLabel = getProviderUsageLabel(selectedProvider); } else if (!hideConnectionMethodMeta) { connectionStatusLabel = 'Not connected'; } @@ -1129,22 +1139,12 @@ export const ProviderRuntimeSettingsDialog = ({ className="text-xs" style={{ color: getProviderStatusColor( - selectedProvider.authenticated - ? `Using ${formatProviderAuthMethodLabelForProvider( - selectedProvider.providerId, - selectedProvider.authMethod - )}` - : selectedProvider.statusMessage || 'Not connected', + getProviderUsageLabel(selectedProvider), selectedProvider.authenticated ), }} > - {selectedProvider.authenticated - ? `Using ${formatProviderAuthMethodLabelForProvider( - selectedProvider.providerId, - selectedProvider.authMethod - )}` - : selectedProvider.statusMessage || 'Not connected'} + {getProviderUsageLabel(selectedProvider)} {managedRuntimeSummary && !hideConnectionMethodMeta ? ( diff --git a/src/renderer/components/runtime/providerConnectionUi.ts b/src/renderer/components/runtime/providerConnectionUi.ts index 6735d371..cf011708 100644 --- a/src/renderer/components/runtime/providerConnectionUi.ts +++ b/src/renderer/components/runtime/providerConnectionUi.ts @@ -119,6 +119,22 @@ function getCodexApiKeyAvailabilitySummary(provider: CliProviderStatus): string return provider.connection.apiKeySourceLabel ?? 'API key is configured'; } +function isAnthropicApiKeyModeReady(provider: CliProviderStatus): boolean { + return ( + provider.providerId === 'anthropic' && + provider.connection?.configuredAuthMode === 'api_key' && + provider.connection.apiKeyConfigured === true + ); +} + +function isAnthropicApiKeyModeMissingCredential(provider: CliProviderStatus): boolean { + return ( + provider.providerId === 'anthropic' && + provider.connection?.configuredAuthMode === 'api_key' && + provider.connection.apiKeyConfigured !== true + ); +} + function getCodexMissingManagedAccountStatus(provider: CliProviderStatus): string | null { if (provider.providerId !== 'codex') { return null; @@ -241,6 +257,14 @@ export function formatProviderStatusText(provider: CliProviderStatus): string { return provider.statusMessage ?? 'Unavailable in current runtime'; } + if (isAnthropicApiKeyModeReady(provider)) { + return 'Connected via API key'; + } + + if (isAnthropicApiKeyModeMissingCredential(provider)) { + return 'API key mode selected, but no API key is configured'; + } + if (provider.authenticated) { return `Connected via ${formatProviderAuthMethodLabelForProvider( provider.providerId, @@ -292,6 +316,10 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin return null; } + if (isAnthropicApiKeyModeReady(provider)) { + return provider.connection?.apiKeySourceLabel ?? 'API key is configured'; + } + if ( provider.providerId === 'anthropic' && provider.connection.apiKeySource === 'stored' && diff --git a/test/main/services/runtime/ProviderConnectionService.test.ts b/test/main/services/runtime/ProviderConnectionService.test.ts index 2b801564..e4e7292b 100644 --- a/test/main/services/runtime/ProviderConnectionService.test.ts +++ b/test/main/services/runtime/ProviderConnectionService.test.ts @@ -211,6 +211,107 @@ describe('ProviderConnectionService', () => { }); }); + it('surfaces stored Anthropic API key mode as the effective provider auth status', async () => { + const { ProviderConnectionService } = + await import('@main/services/runtime/ProviderConnectionService'); + + const service = new ProviderConnectionService( + { + lookupPreferred: vi.fn().mockResolvedValue({ + envVarName: 'ANTHROPIC_API_KEY', + value: 'stored-key', + }), + } as never, + { + getConfig: () => createConfig('api_key'), + } as never + ); + + const status = await service.enrichProviderStatus({ + providerId: 'anthropic', + displayName: 'Anthropic', + supported: true, + authenticated: true, + authMethod: 'claude.ai', + verificationState: 'verified', + statusMessage: 'Connected via Anthropic subscription', + models: ['claude-sonnet-4-6'], + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + extensions: { mcp: 'unsupported', skills: 'unsupported', plugins: 'unsupported' }, + }, + selectedBackendId: null, + resolvedBackendId: null, + availableBackends: [], + externalRuntimeDiagnostics: [], + backend: null, + connection: null, + } as never); + + expect(status).toMatchObject({ + authenticated: true, + authMethod: 'api_key', + verificationState: 'verified', + statusMessage: 'Connected via API key', + connection: { + configuredAuthMode: 'api_key', + apiKeyConfigured: true, + apiKeySource: 'stored', + apiKeySourceLabel: 'Stored in app', + }, + }); + }); + + it('does not treat a subscription session as connected when Anthropic API key mode has no key', async () => { + const { ProviderConnectionService } = + await import('@main/services/runtime/ProviderConnectionService'); + + const service = new ProviderConnectionService( + { + lookupPreferred: vi.fn().mockResolvedValue(null), + } as never, + { + getConfig: () => createConfig('api_key'), + } as never + ); + + const status = await service.enrichProviderStatus({ + providerId: 'anthropic', + displayName: 'Anthropic', + supported: true, + authenticated: true, + authMethod: 'claude.ai', + verificationState: 'verified', + statusMessage: 'Connected via Anthropic subscription', + models: ['claude-sonnet-4-6'], + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + extensions: { mcp: 'unsupported', skills: 'unsupported', plugins: 'unsupported' }, + }, + selectedBackendId: null, + resolvedBackendId: null, + availableBackends: [], + externalRuntimeDiagnostics: [], + backend: null, + connection: null, + } as never); + + expect(status).toMatchObject({ + authenticated: false, + authMethod: null, + verificationState: 'unknown', + statusMessage: 'API key mode is selected, but no Anthropic API credential is available yet.', + connection: { + configuredAuthMode: 'api_key', + apiKeyConfigured: false, + }, + }); + }); + it('exposes Codex as native-only API-key runtime', async () => { const { ProviderConnectionService } = await import('@main/services/runtime/ProviderConnectionService'); diff --git a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts index 71691049..ab3af948 100644 --- a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts +++ b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts @@ -577,6 +577,54 @@ describe('ProviderRuntimeSettingsDialog', () => { expect(onRefreshProvider).toHaveBeenCalledWith('anthropic'); }); + it('shows Anthropic API key usage when API key mode is selected even if the local CLI has a subscription session', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + storeState.appConfig.providerConnections.anthropic.authMode = 'api_key'; + storeState.apiKeys = [ + { + id: 'anthropic-key', + envVarName: 'ANTHROPIC_API_KEY', + scope: 'user', + name: 'Anthropic API Key', + maskedValue: 'sk-ant-...', + }, + ]; + + await act(async () => { + root.render( + React.createElement(ProviderRuntimeSettingsDialog, { + open: true, + onOpenChange: vi.fn(), + providers: [ + createAnthropicProvider({ + authenticated: true, + authMethod: 'claude.ai', + configuredAuthMode: 'api_key', + apiKeyConfigured: true, + apiKeySource: 'stored', + apiKeySourceLabel: 'Stored in app', + }), + ], + initialProviderId: 'anthropic', + onSelectBackend: vi.fn(), + onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('Using API key'); + expect(host.textContent).not.toContain('Using Anthropic subscription'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + host.remove(); + }); + it('accepts and saves a typed Anthropic API key from provider settings', async () => { const host = document.createElement('div'); document.body.appendChild(host); diff --git a/test/renderer/components/runtime/providerConnectionUi.test.ts b/test/renderer/components/runtime/providerConnectionUi.test.ts index 7e63ab0b..a89682c1 100644 --- a/test/renderer/components/runtime/providerConnectionUi.test.ts +++ b/test/renderer/components/runtime/providerConnectionUi.test.ts @@ -184,6 +184,36 @@ describe('providerConnectionUi', () => { expect(getProviderConnectionModeSummary(provider)).toBeNull(); }); + it('shows Anthropic API key as the effective connection when API key mode is pinned', () => { + const provider = createAnthropicProvider({ + authenticated: true, + authMethod: 'claude.ai', + configuredAuthMode: 'api_key', + apiKeyConfigured: true, + apiKeySource: 'stored', + apiKeySourceLabel: 'Stored in app', + }); + + expect(formatProviderStatusText(provider)).toBe('Connected via API key'); + expect(getProviderCredentialSummary(provider)).toBe('Stored in app'); + }); + + it('does not describe Anthropic API key mode as subscription connected when the key is missing', () => { + const provider = createAnthropicProvider({ + authenticated: true, + authMethod: 'claude.ai', + configuredAuthMode: 'api_key', + apiKeyConfigured: false, + apiKeySource: null, + apiKeySourceLabel: null, + }); + + expect(formatProviderStatusText(provider)).toBe( + 'API key mode selected, but no API key is configured' + ); + expect(getProviderCredentialSummary(provider)).toBeNull(); + }); + it('shows Anthropic preferred auth summary when a pinned mode is selected but not connected', () => { const provider = createAnthropicProvider({ authenticated: false,