fix: show effective Anthropic API key status

This commit is contained in:
iliya 2026-04-30 22:21:29 +03:00
parent bab17e91ba
commit 62a1b0e865
6 changed files with 253 additions and 16 deletions

View file

@ -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<CliProviderStatus[]> {
return Promise.all(providers.map((provider) => this.enrichProviderStatus(provider)));
}

View file

@ -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)}
</span>
{managedRuntimeSummary && !hideConnectionMethodMeta ? (
<span className="text-xs" style={{ color: 'var(--color-text-secondary)' }}>

View file

@ -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' &&

View file

@ -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');

View file

@ -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);

View file

@ -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,