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),
|
connection: await this.getConnectionInfo(provider.providerId),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (provider.providerId === 'anthropic') {
|
||||||
|
return this.enrichAnthropicProviderStatus(withConnection);
|
||||||
|
}
|
||||||
|
|
||||||
if (provider.providerId !== 'codex') {
|
if (provider.providerId !== 'codex') {
|
||||||
return withConnection;
|
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[]> {
|
async enrichProviderStatuses(providers: CliProviderStatus[]): Promise<CliProviderStatus[]> {
|
||||||
return Promise.all(providers.map((provider) => this.enrichProviderStatus(provider)));
|
return Promise.all(providers.map((provider) => this.enrichProviderStatus(provider)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,19 @@ function getConnectionAlert(provider: CliProviderStatus): string | null {
|
||||||
return 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(
|
function getCodexAccountPanelHint(
|
||||||
provider: CliProviderStatus | null,
|
provider: CliProviderStatus | null,
|
||||||
configuredAuthMode: CliProviderAuthMode | undefined
|
configuredAuthMode: CliProviderAuthMode | undefined
|
||||||
|
|
@ -818,10 +831,7 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
let connectionStatusLabel: string | null = null;
|
let connectionStatusLabel: string | null = null;
|
||||||
if (selectedProvider) {
|
if (selectedProvider) {
|
||||||
if (!hideConnectionMethodMeta && selectedProvider.authenticated) {
|
if (!hideConnectionMethodMeta && selectedProvider.authenticated) {
|
||||||
connectionStatusLabel = `Using ${formatProviderAuthMethodLabelForProvider(
|
connectionStatusLabel = getProviderUsageLabel(selectedProvider);
|
||||||
selectedProvider.providerId,
|
|
||||||
selectedProvider.authMethod
|
|
||||||
)}`;
|
|
||||||
} else if (!hideConnectionMethodMeta) {
|
} else if (!hideConnectionMethodMeta) {
|
||||||
connectionStatusLabel = 'Not connected';
|
connectionStatusLabel = 'Not connected';
|
||||||
}
|
}
|
||||||
|
|
@ -1129,22 +1139,12 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
style={{
|
style={{
|
||||||
color: getProviderStatusColor(
|
color: getProviderStatusColor(
|
||||||
selectedProvider.authenticated
|
getProviderUsageLabel(selectedProvider),
|
||||||
? `Using ${formatProviderAuthMethodLabelForProvider(
|
|
||||||
selectedProvider.providerId,
|
|
||||||
selectedProvider.authMethod
|
|
||||||
)}`
|
|
||||||
: selectedProvider.statusMessage || 'Not connected',
|
|
||||||
selectedProvider.authenticated
|
selectedProvider.authenticated
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedProvider.authenticated
|
{getProviderUsageLabel(selectedProvider)}
|
||||||
? `Using ${formatProviderAuthMethodLabelForProvider(
|
|
||||||
selectedProvider.providerId,
|
|
||||||
selectedProvider.authMethod
|
|
||||||
)}`
|
|
||||||
: selectedProvider.statusMessage || 'Not connected'}
|
|
||||||
</span>
|
</span>
|
||||||
{managedRuntimeSummary && !hideConnectionMethodMeta ? (
|
{managedRuntimeSummary && !hideConnectionMethodMeta ? (
|
||||||
<span className="text-xs" style={{ color: 'var(--color-text-secondary)' }}>
|
<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';
|
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 {
|
function getCodexMissingManagedAccountStatus(provider: CliProviderStatus): string | null {
|
||||||
if (provider.providerId !== 'codex') {
|
if (provider.providerId !== 'codex') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -241,6 +257,14 @@ export function formatProviderStatusText(provider: CliProviderStatus): string {
|
||||||
return provider.statusMessage ?? 'Unavailable in current runtime';
|
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) {
|
if (provider.authenticated) {
|
||||||
return `Connected via ${formatProviderAuthMethodLabelForProvider(
|
return `Connected via ${formatProviderAuthMethodLabelForProvider(
|
||||||
provider.providerId,
|
provider.providerId,
|
||||||
|
|
@ -292,6 +316,10 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAnthropicApiKeyModeReady(provider)) {
|
||||||
|
return provider.connection?.apiKeySourceLabel ?? 'API key is configured';
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
provider.providerId === 'anthropic' &&
|
provider.providerId === 'anthropic' &&
|
||||||
provider.connection.apiKeySource === 'stored' &&
|
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 () => {
|
it('exposes Codex as native-only API-key runtime', async () => {
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,54 @@ describe('ProviderRuntimeSettingsDialog', () => {
|
||||||
expect(onRefreshProvider).toHaveBeenCalledWith('anthropic');
|
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 () => {
|
it('accepts and saves a typed Anthropic API key from provider settings', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,36 @@ describe('providerConnectionUi', () => {
|
||||||
expect(getProviderConnectionModeSummary(provider)).toBeNull();
|
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', () => {
|
it('shows Anthropic preferred auth summary when a pinned mode is selected but not connected', () => {
|
||||||
const provider = createAnthropicProvider({
|
const provider = createAnthropicProvider({
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue