fix: show effective Anthropic API key status
This commit is contained in:
parent
bab17e91ba
commit
62a1b0e865
6 changed files with 253 additions and 16 deletions
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)' }}>
|
||||
|
|
|
|||
|
|
@ -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' &&
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue