import { useEffect, useMemo, useState } from 'react'; import { formatCodexCreditsValue, formatCodexRemainingPercent, formatCodexResetWindowLabel, formatCodexUsageExplanation, formatCodexUsagePercent, formatCodexUsageWindowLabel, formatCodexWindowDurationLong, mergeCodexProviderStatusWithSnapshot, normalizeCodexResetTimestamp, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; import { CODEX_FAST_CREDIT_COST_MULTIPLIER, CODEX_FAST_MODEL_ID, CODEX_FAST_SPEED_MULTIPLIER, resolveCodexFastMode, resolveCodexRuntimeSelection, } from '@features/codex-runtime-profile/renderer'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; import { useStore } from '@renderer/store'; import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react'; import { formatProviderAuthMethodLabelForProvider, formatProviderAuthModeLabelForProvider, getProviderConnectLabel, getProviderCurrentRuntimeSummary, isConnectionManagedRuntimeProvider, } from './providerConnectionUi'; import { getProviderRuntimeBackendSummary, getVisibleProviderRuntimeBackendOptions, ProviderRuntimeBackendSelector, } from './ProviderRuntimeBackendSelector'; import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types'; import type { ApiKeyEntry } from '@shared/types/extensions'; type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini'; type PendingConnectionAction = 'auto' | 'oauth' | 'chatgpt' | 'api_key' | null; interface ConnectionMethodCardOption { readonly authMode: CliProviderAuthMode; readonly title: string; readonly description: string; } interface Props { readonly open: boolean; readonly onOpenChange: (open: boolean) => void; readonly providers: CliProviderStatus[]; readonly initialProviderId: CliProviderId; readonly providerStatusLoading?: Partial>; readonly disabled?: boolean; readonly onSelectBackend: (providerId: CliProviderId, backendId: string) => Promise | void; readonly onRefreshProvider?: (providerId: CliProviderId) => Promise | void; readonly onRequestLogin?: (providerId: CliProviderId) => void; } const API_KEY_PROVIDER_CONFIG: Record< ApiKeyProviderId, { envVarName: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY'; name: string; title: string; description: string; placeholder: string; } > = { anthropic: { envVarName: 'ANTHROPIC_API_KEY', name: 'Anthropic API Key', title: 'API key', description: 'Use a direct Anthropic API key for API-billed access. Your Anthropic subscription session stays available when you switch back.', placeholder: 'sk-ant-...', }, codex: { envVarName: 'OPENAI_API_KEY', name: 'Codex API Key', title: 'API key', description: 'Use an OpenAI API key as a secondary Codex auth path. If you switch Codex to API key mode, the app will mirror OPENAI_API_KEY into CODEX_API_KEY for native launches.', placeholder: 'sk-proj-...', }, gemini: { envVarName: 'GEMINI_API_KEY', name: 'Gemini API Key', title: 'API access', description: 'Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.', placeholder: 'AIza...', }, }; function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProviderId { return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini'; } function findPreferredApiKeyEntry(apiKeys: ApiKeyEntry[], envVarName: string): ApiKeyEntry | null { const matches = apiKeys.filter((entry) => entry.envVarName === envVarName); return matches.find((entry) => entry.scope === 'user') ?? null; } function getConnectionDescription(provider: CliProviderStatus): string { switch (provider.providerId) { case 'anthropic': return 'Choose how app-launched Anthropic sessions authenticate.'; case 'codex': return 'Choose whether Codex should prefer your ChatGPT subscription or an API key when the native runtime launches.'; case 'gemini': return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.'; case 'opencode': return 'OpenCode authentication and provider inventory are managed by the OpenCode runtime.'; } } function getRuntimeDescription(provider: CliProviderStatus): string { switch (provider.providerId) { case 'anthropic': return 'Anthropic currently has no separate runtime backend selector.'; case 'codex': return 'Codex now runs only through the native runtime path.'; case 'gemini': return 'Choose which Gemini runtime backend multimodel should use.'; case 'opencode': return 'OpenCode uses its own managed runtime host. Desktop currently exposes status only.'; } } function getAuthModeDescription(providerId: CliProviderId, authMode: CliProviderAuthMode): string { if (providerId === 'anthropic') { switch (authMode) { case 'auto': return 'Use the runtime default behavior. Saved API keys in this app are only used after you switch to API key mode.'; case 'oauth': return 'Force app-launched Anthropic sessions to use the local Anthropic subscription session.'; case 'api_key': return 'Force app-launched Anthropic sessions to use an API key credential.'; } } if (providerId === 'codex') { switch (authMode) { case 'auto': return 'Prefer your ChatGPT account when it is available. Fall back to API key mode only when needed.'; case 'chatgpt': return 'Force native Codex launches to use your connected ChatGPT account and subscription.'; case 'api_key': return 'Force native Codex launches to use OPENAI_API_KEY / CODEX_API_KEY billing.'; default: return ''; } } return ''; } function getConnectionAlert(provider: CliProviderStatus): string | null { const authMode = provider.connection?.configuredAuthMode; const hasAnthropicSubscriptionSession = provider.authMethod === 'oauth_token' || provider.authMethod === 'claude.ai'; if ( provider.providerId === 'anthropic' && authMode === 'api_key' && !provider.connection?.apiKeyConfigured ) { return 'API key mode is selected, but no Anthropic API credential is available yet.'; } if ( provider.providerId === 'anthropic' && authMode === 'oauth' && !hasAnthropicSubscriptionSession ) { return 'Anthropic subscription mode is selected. Sign in with Anthropic to use this provider.'; } if ( provider.providerId === 'anthropic' && authMode === 'auto' && provider.connection?.apiKeySource === 'stored' ) { return 'A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.'; } if (provider.providerId === 'codex') { const codex = provider.connection?.codex; if (codex?.login.status === 'starting') { return 'Starting ChatGPT login...'; } if (codex?.login.status === 'pending') { return 'Waiting for ChatGPT account login to finish...'; } if (codex?.login.status === 'failed' && codex.login.error) { return codex.login.error; } if (provider.connection?.configuredAuthMode === 'api_key') { if (!provider.connection?.apiKeyConfigured) { return 'API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.'; } return null; } if (provider.connection?.configuredAuthMode === 'chatgpt' && !codex?.managedAccount) { const missingChatgptMessage = codex?.localActiveChatgptAccountPresent ? 'Codex has a locally selected ChatGPT account, but the current session needs reconnect.' : codex?.localAccountArtifactsPresent ? 'Codex CLI currently has no active ChatGPT account. Local Codex account data exists, but no active managed session is selected.' : 'Codex CLI currently has no active ChatGPT account. Connect ChatGPT to use your subscription.'; return provider.connection.apiKeyConfigured ? `${missingChatgptMessage} Switch to API key mode to use the detected API key.` : missingChatgptMessage; } if (!codex?.launchAllowed && codex?.launchIssueMessage) { return codex.launchIssueMessage; } if (codex?.appServerState === 'degraded' && codex.appServerStatusMessage) { return codex.appServerStatusMessage; } if (!provider.connection?.apiKeyConfigured && !codex?.managedAccount) { return 'No ChatGPT account or API key is available yet.'; } return null; } if ( provider.providerId === 'gemini' && provider.availableBackends?.some((option) => option.id === 'api' && !option.available) ) { return 'Gemini API is currently unavailable. Configure `GEMINI_API_KEY` here or use valid Google ADC credentials.'; } return null; } function getCodexAccountPanelHint( provider: CliProviderStatus | null, configuredAuthMode: CliProviderAuthMode | undefined ): string | null { if (provider?.providerId !== 'codex') { return null; } const codex = provider.connection?.codex; if (!codex || codex.login.status === 'starting' || codex.login.status === 'pending') { return null; } const hasActiveChatgptSession = codex.effectiveAuthMode === 'chatgpt' && codex.launchAllowed === true; if (hasActiveChatgptSession) { if (!codex.rateLimits) { return 'Usage limits appear here after Codex reports them for the connected ChatGPT account.'; } return null; } const usageSentence = codex.localActiveChatgptAccountPresent ? 'Codex has a locally selected ChatGPT account, but the current session needs reconnect before usage limits can load here.' : codex.localAccountArtifactsPresent ? 'Codex CLI currently reports no active ChatGPT account. Local Codex account data exists, but no active managed session is selected. Usage limits appear here only after Codex CLI sees one.' : 'Codex CLI currently reports no active ChatGPT account. Usage limits appear here only after Codex CLI sees one.'; if (configuredAuthMode === 'chatgpt' && provider.connection?.apiKeyConfigured) { return `${usageSentence} The detected API key is only used after you switch Codex to API key mode.`; } if (configuredAuthMode === 'auto' && provider.connection?.apiKeyConfigured) { return `${usageSentence} Auto will keep using the detected API key until ChatGPT is connected.`; } return usageSentence; } function getCheckingStatusColor(): string { return 'var(--color-text-secondary)'; } function getProviderStatusColor(statusText: string | null, authenticated: boolean): string { if (statusText === 'Checking...') { return getCheckingStatusColor(); } return authenticated ? '#4ade80' : 'var(--color-text-muted)'; } function formatCodexResetDateTime(timestampSeconds: number | null | undefined): string { const normalized = normalizeCodexResetTimestamp(timestampSeconds); return normalized ? new Date(normalized).toLocaleString() : 'Unknown'; } function CodexRateLimitWindowCard({ title, usedLabel, usedValue, remainingValue, resetLabel, resetValue, accent, }: Readonly<{ title: string; usedLabel: string; usedValue: string; remainingValue: string; resetLabel: string; resetValue: string; accent: 'primary' | 'secondary'; }>): React.JSX.Element { const accentStyles = accent === 'primary' ? { borderColor: 'rgba(74, 222, 128, 0.24)', backgroundColor: 'rgba(74, 222, 128, 0.05)', badgeColor: '#86efac', badgeBackground: 'rgba(74, 222, 128, 0.14)', } : { borderColor: 'rgba(125, 211, 252, 0.22)', backgroundColor: 'rgba(125, 211, 252, 0.04)', badgeColor: '#bae6fd', badgeBackground: 'rgba(125, 211, 252, 0.14)', }; return (
{title}
{remainingValue}
{usedLabel}
{usedValue}
{remainingValue} left
{resetLabel}
{resetValue}
); } function getConnectionMethodCardOptions( provider: CliProviderStatus ): ConnectionMethodCardOption[] | null { switch (provider.providerId) { case 'anthropic': return [ { authMode: 'auto', title: 'Auto', description: 'Use Anthropic runtime defaults and the best local credential available.', }, { authMode: 'oauth', title: 'Anthropic subscription', description: 'Use your local Anthropic sign-in session and subscription access.', }, { authMode: 'api_key', title: 'API key', description: 'Use ANTHROPIC_API_KEY and Anthropic API billing.', }, ]; case 'codex': return [ { authMode: 'auto', title: 'Auto', description: 'Prefer your ChatGPT account and subscription. Use API key mode only if needed.', }, { authMode: 'chatgpt', title: 'ChatGPT account', description: 'Use your connected ChatGPT account and Codex subscription.', }, { authMode: 'api_key', title: 'API key', description: 'Use OPENAI_API_KEY and CODEX_API_KEY billing for native Codex launches.', }, ]; default: return null; } } function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null { if (provider.providerId === 'codex') { return 'Codex always runs through the native runtime. Auto prefers your ChatGPT account before falling back to API-key credentials.'; } if (provider.providerId === 'anthropic') { return 'Auto keeps Anthropic on its default local credential resolution.'; } return null; } const ConnectionMethodCards = ({ options, selectedAuthMode, disabled, connectionSaving, pendingConnectionAction, onSelect, }: Readonly<{ options: ConnectionMethodCardOption[]; selectedAuthMode: CliProviderAuthMode; disabled: boolean; connectionSaving: boolean; pendingConnectionAction: PendingConnectionAction; onSelect: (authMode: CliProviderAuthMode) => void; }>): React.JSX.Element => { const gridClassName = options.length === 3 ? 'grid gap-2 md:grid-cols-3' : 'grid gap-2 sm:grid-cols-2'; return (
{options.map((option) => { const selected = selectedAuthMode === option.authMode; return ( ); })}
); }; export const ProviderRuntimeSettingsDialog = ({ open, onOpenChange, providers, initialProviderId, providerStatusLoading = {}, disabled = false, onSelectBackend, onRefreshProvider, onRequestLogin, }: Props): React.JSX.Element => { const [selectedProviderId, setSelectedProviderId] = useState(initialProviderId); const [activeApiKeyFormProviderId, setActiveApiKeyFormProviderId] = useState(null); const [apiKeyValue, setApiKeyValue] = useState(''); const [apiKeyScope, setApiKeyScope] = useState<'user' | 'project'>('user'); const [apiKeyError, setApiKeyError] = useState(null); const [connectionError, setConnectionError] = useState(null); const [runtimeError, setRuntimeError] = useState(null); const [connectionSaving, setConnectionSaving] = useState(false); const [runtimeSaving, setRuntimeSaving] = useState(false); const [pendingConnectionAction, setPendingConnectionAction] = useState(null); const apiKeys = useStore((s) => s.apiKeys); const apiKeysLoading = useStore((s) => s.apiKeysLoading); const apiKeysError = useStore((s) => s.apiKeysError); const apiKeySaving = useStore((s) => s.apiKeySaving); const apiKeyStorageStatus = useStore((s) => s.apiKeyStorageStatus); const fetchApiKeys = useStore((s) => s.fetchApiKeys); const fetchApiKeyStorageStatus = useStore((s) => s.fetchApiKeyStorageStatus); const saveApiKey = useStore((s) => s.saveApiKey); const deleteApiKey = useStore((s) => s.deleteApiKey); const updateConfig = useStore((s) => s.updateConfig); const appConfig = useStore((s) => s.appConfig); const codexAccount = useCodexAccountSnapshot({ enabled: open && selectedProviderId === 'codex', includeRateLimits: true, }); useEffect(() => { if (!open) { return; } setSelectedProviderId(initialProviderId); void fetchApiKeys(); void fetchApiKeyStorageStatus(); }, [fetchApiKeyStorageStatus, fetchApiKeys, initialProviderId, open]); useEffect(() => { if (open) { return; } setActiveApiKeyFormProviderId(null); setApiKeyValue(''); setApiKeyScope('user'); setApiKeyError(null); setConnectionError(null); setRuntimeError(null); setConnectionSaving(false); setRuntimeSaving(false); setPendingConnectionAction(null); }, [open]); useEffect(() => { setConnectionError(null); setRuntimeError(null); }, [selectedProviderId]); useEffect(() => { if (selectedProviderId === 'codex' && codexAccount.error) { setConnectionError(codexAccount.error); } }, [codexAccount.error, selectedProviderId]); const statusSelectedProvider = useMemo(() => { return ( providers.find((provider) => provider.providerId === selectedProviderId) ?? providers.find( (provider) => provider.availableBackends && provider.availableBackends.length > 0 ) ?? providers[0] ?? null ); }, [providers, selectedProviderId]); const statusApiKeyConfig = statusSelectedProvider && isApiKeyProviderId(statusSelectedProvider.providerId) ? API_KEY_PROVIDER_CONFIG[statusSelectedProvider.providerId] : null; const selectedApiKey = statusApiKeyConfig ? findPreferredApiKeyEntry(apiKeys, statusApiKeyConfig.envVarName) : null; const selectedProvider = useMemo(() => { const mergedStatusProvider = statusSelectedProvider?.providerId === 'codex' ? mergeCodexProviderStatusWithSnapshot(statusSelectedProvider, codexAccount.snapshot) : statusSelectedProvider; if (!mergedStatusProvider?.connection) { return mergedStatusProvider; } const nextConnection = { ...mergedStatusProvider.connection, }; if (mergedStatusProvider.providerId === 'anthropic') { nextConnection.configuredAuthMode = appConfig?.providerConnections?.anthropic.authMode ?? mergedStatusProvider.connection.configuredAuthMode; } if (mergedStatusProvider.providerId === 'codex') { nextConnection.configuredAuthMode = appConfig?.providerConnections?.codex.preferredAuthMode ?? mergedStatusProvider.connection.configuredAuthMode; } if (statusApiKeyConfig) { if (nextConnection.apiKeySource === 'stored') { nextConnection.apiKeyConfigured = Boolean(selectedApiKey); nextConnection.apiKeySource = selectedApiKey ? 'stored' : null; nextConnection.apiKeySourceLabel = selectedApiKey ? 'Stored in app' : null; } else if (!nextConnection.apiKeyConfigured && selectedApiKey) { nextConnection.apiKeyConfigured = true; nextConnection.apiKeySource = 'stored'; nextConnection.apiKeySourceLabel = 'Stored in app'; } } return { ...mergedStatusProvider, connection: nextConnection, }; }, [ appConfig?.providerConnections?.anthropic.authMode, appConfig?.providerConnections?.codex.preferredAuthMode, codexAccount.snapshot, selectedApiKey, statusApiKeyConfig, statusSelectedProvider, ]); const selectedProviderLoading = selectedProvider ? providerStatusLoading[selectedProvider.providerId] === true : false; const runtimeSummary = selectedProvider ? getProviderRuntimeBackendSummary(selectedProvider) : null; const codexConnection = selectedProvider?.providerId === 'codex' ? (selectedProvider.connection?.codex ?? null) : null; const codexHasActiveChatgptSession = codexConnection?.effectiveAuthMode === 'chatgpt' && codexConnection.launchAllowed === true; const codexNeedsReconnect = Boolean(codexConnection?.localActiveChatgptAccountPresent) && !codexHasActiveChatgptSession; const codexLoginPending = codexConnection?.login.status === 'starting' || codexConnection?.login.status === 'pending'; const configurableAuthModes = selectedProvider?.connection?.configurableAuthModes ?? []; const configuredAuthMode: CliProviderAuthMode | undefined = selectedProvider?.connection?.configuredAuthMode ?? configurableAuthModes[0] ?? undefined; const connectionMethodCardOptions = selectedProvider ? getConnectionMethodCardOptions(selectedProvider) : null; const showConnectionMethodCards = connectionMethodCardOptions !== null && typeof configuredAuthMode !== 'undefined'; const managedRuntimeSummary = selectedProvider ? getProviderCurrentRuntimeSummary(selectedProvider) : null; const connectionManagedRuntime = selectedProvider ? isConnectionManagedRuntimeProvider(selectedProvider) : false; const hideConnectionMethodMeta = showConnectionMethodCards; const canConfigureRuntime = !connectionManagedRuntime && (selectedProvider ? getVisibleProviderRuntimeBackendOptions(selectedProvider).length > 1 : false); const apiKeyConfig = selectedProvider && isApiKeyProviderId(selectedProvider.providerId) ? API_KEY_PROVIDER_CONFIG[selectedProvider.providerId] : null; const showApiKeyForm = selectedProvider && isApiKeyProviderId(selectedProvider.providerId) && activeApiKeyFormProviderId === selectedProvider.providerId; const showApiKeySection = Boolean( apiKeyConfig && (selectedProvider?.providerId !== 'codex' || !selectedProvider.connection?.supportsOAuth) ); const connectionAlert = selectedProvider ? getConnectionAlert(selectedProvider) : null; const connectionLoading = selectedProviderLoading || connectionSaving || Boolean(selectedProvider?.providerId === 'codex' && codexAccount.loading && !codexConnection); const connectionBusy = disabled || connectionLoading; const codexActionBusy = disabled || selectedProviderLoading || connectionSaving || codexAccount.loading; const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving; const anthropicFastModeCapability = selectedProvider?.providerId === 'anthropic' ? (selectedProvider.runtimeCapabilities?.fastMode ?? null) : null; const anthropicFastModeEnabled = appConfig?.providerConnections?.anthropic.fastModeDefault === true; const anthropicFastModeSupported = anthropicFastModeCapability?.supported === true; const anthropicFastModeAvailable = anthropicFastModeCapability?.available === true; const anthropicFastModeDisabledReason = anthropicFastModeCapability?.reason ?? (anthropicFastModeSupported ? 'Fast mode is currently unavailable for this Anthropic runtime.' : 'This Anthropic runtime does not expose Fast mode.'); const connectionMethodCardsHint = selectedProvider ? getConnectionMethodCardsHint(selectedProvider) : null; const codexAccountPanelHint = getCodexAccountPanelHint( selectedProvider ?? null, configuredAuthMode ); const codexFastCapability = useMemo(() => { if (selectedProvider?.providerId !== 'codex') { return null; } const fastProbeModel = selectedProvider.modelCatalog?.models.find((model) => model.supportsFastMode === true) ?.launchModel ?? CODEX_FAST_MODEL_ID; const selection = resolveCodexRuntimeSelection({ source: { providerStatus: selectedProvider, accountSnapshot: codexAccount.snapshot, }, selectedModel: fastProbeModel, }); return resolveCodexFastMode({ selection, selectedFastMode: 'on', }); }, [codexAccount.snapshot, selectedProvider]); const codexFastCapabilityHint = selectedProvider?.providerId === 'codex' && codexFastCapability ? codexFastCapability.selectable ? `Fast mode can be enabled per team or schedule for Fast-capable Codex models with your ChatGPT account. It is about ${CODEX_FAST_SPEED_MULTIPLIER}x faster and costs ${CODEX_FAST_CREDIT_COST_MULTIPLIER}x credits.` : (codexFastCapability.disabledReason ?? 'Codex Fast mode is currently unavailable for this account or runtime.') : null; const hasSubscriptionSession = selectedProvider?.providerId === 'anthropic' ? selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai' : false; const canRequestSubscriptionLogin = selectedProvider?.providerId === 'anthropic' && Boolean(selectedProvider.connection?.supportsOAuth && onRequestLogin) && configuredAuthMode !== 'api_key' && selectedProvider.statusMessage !== 'Checking...' && (!selectedProvider?.authenticated || hasSubscriptionSession || configuredAuthMode === 'oauth'); let connectionStatusLabel: string | null = null; if (selectedProvider) { if (!hideConnectionMethodMeta && selectedProvider.authenticated) { connectionStatusLabel = `Using ${formatProviderAuthMethodLabelForProvider( selectedProvider.providerId, selectedProvider.authMethod )}`; } else if (!hideConnectionMethodMeta) { connectionStatusLabel = 'Not connected'; } } const showSelectedProviderSummary = Boolean(selectedProvider) && !connectionManagedRuntime; const connectionProgressMessage = useMemo(() => { if (!connectionLoading || !selectedProvider) { return null; } if (connectionSaving) { if (selectedProvider.providerId === 'anthropic') { switch (pendingConnectionAction) { case 'api_key': return 'Switching to API key...'; case 'oauth': return 'Switching to Anthropic subscription...'; case 'auto': return 'Switching to Auto...'; default: return 'Applying connection changes...'; } } if (selectedProvider.providerId === 'codex') { switch (pendingConnectionAction) { case 'chatgpt': return 'Switching to ChatGPT account mode...'; case 'api_key': return 'Switching to API key mode...'; case 'auto': return 'Switching to Auto...'; default: return 'Applying connection changes...'; } } return 'Applying connection changes...'; } return 'Refreshing provider status...'; }, [connectionLoading, connectionSaving, pendingConnectionAction, selectedProvider]); const handleStartApiKeyEdit = (): void => { if (!selectedProvider || !isApiKeyProviderId(selectedProvider.providerId) || !apiKeyConfig) { return; } setConnectionError(null); setActiveApiKeyFormProviderId(selectedProvider.providerId); setApiKeyScope(selectedApiKey?.scope ?? 'user'); setApiKeyValue(''); setApiKeyError(null); }; const handleCancelApiKeyEdit = (): void => { setActiveApiKeyFormProviderId(null); setApiKeyValue(''); setApiKeyError(null); }; const handleSaveApiKey = async (): Promise => { if (!selectedProvider || !isApiKeyProviderId(selectedProvider.providerId) || !apiKeyConfig) { return; } if (!apiKeyValue.trim()) { setApiKeyError('API key is required'); return; } setApiKeyError(null); setConnectionError(null); try { await saveApiKey({ id: selectedApiKey?.id, name: apiKeyConfig.name, envVarName: apiKeyConfig.envVarName, value: apiKeyValue.trim(), scope: apiKeyScope, }); } catch (error) { setApiKeyError(error instanceof Error ? error.message : 'Failed to save API key'); return; } setActiveApiKeyFormProviderId(null); setApiKeyValue(''); try { await onRefreshProvider?.(selectedProvider.providerId); } catch { setConnectionError('API key saved, but failed to refresh provider status.'); } }; const handleDeleteApiKey = async (): Promise => { if (!selectedProvider || !selectedApiKey) { return; } setApiKeyError(null); setConnectionError(null); try { await deleteApiKey(selectedApiKey.id); } catch (error) { setApiKeyError(error instanceof Error ? error.message : 'Failed to delete API key'); return; } setActiveApiKeyFormProviderId(null); setApiKeyValue(''); try { await onRefreshProvider?.(selectedProvider.providerId); } catch { setConnectionError('API key deleted, but failed to refresh provider status.'); } }; const handleAuthModeChange = async (authMode: string): Promise => { if (selectedProvider?.providerId !== 'anthropic' && selectedProvider?.providerId !== 'codex') { return; } const nextAuthMode = authMode as CliProviderAuthMode; if (nextAuthMode === configuredAuthMode) { return; } setConnectionSaving(true); setPendingConnectionAction(nextAuthMode); setConnectionError(null); let updateSucceeded = false; try { if (selectedProvider.providerId === 'anthropic') { await updateConfig('providerConnections', { anthropic: { authMode: nextAuthMode, }, }); } else if (nextAuthMode !== 'oauth') { await updateConfig('providerConnections', { codex: { preferredAuthMode: nextAuthMode, }, }); await codexAccount.refresh({ includeRateLimits: true, forceRefreshToken: true }); } updateSucceeded = true; } catch (error) { setConnectionError(error instanceof Error ? error.message : 'Failed to update connection'); } finally { if (updateSucceeded) { try { await onRefreshProvider?.(selectedProvider.providerId); } catch { setConnectionError('Connection updated, but failed to refresh provider status.'); } } setConnectionSaving(false); setPendingConnectionAction(null); } }; const handleCodexAccountRefresh = async (): Promise => { setConnectionError(null); try { await codexAccount.refresh({ includeRateLimits: true, forceRefreshToken: true }); await onRefreshProvider?.('codex'); } catch (error) { setConnectionError( error instanceof Error ? error.message : 'Failed to refresh Codex account' ); } }; const handleCodexStartLogin = async (): Promise => { setConnectionError(null); const success = await codexAccount.startChatgptLogin(); if (!success && codexAccount.error) { setConnectionError(codexAccount.error); } }; const handleCodexCancelLogin = async (): Promise => { setConnectionError(null); const success = await codexAccount.cancelChatgptLogin(); if (success) { await onRefreshProvider?.('codex'); } else if (codexAccount.error) { setConnectionError(codexAccount.error); } }; const handleCodexLogout = async (): Promise => { setConnectionError(null); const success = await codexAccount.logout(); if (success) { await onRefreshProvider?.('codex'); } else if (codexAccount.error) { setConnectionError(codexAccount.error); } }; const handleRuntimeBackendSelect = async ( providerId: CliProviderId, backendId: string ): Promise => { setRuntimeSaving(true); setRuntimeError(null); try { await onSelectBackend(providerId, backendId); } catch (error) { setRuntimeError(error instanceof Error ? error.message : 'Failed to update runtime backend'); } finally { setRuntimeSaving(false); } }; const handleAnthropicFastModeDefaultChange = async (enabled: boolean): Promise => { if (selectedProvider?.providerId !== 'anthropic' || anthropicFastModeEnabled === enabled) { return; } setConnectionSaving(true); setConnectionError(null); try { await updateConfig('providerConnections', { anthropic: { fastModeDefault: enabled, }, }); await onRefreshProvider?.('anthropic'); } catch (error) { setConnectionError( error instanceof Error ? error.message : 'Failed to update Anthropic Fast mode' ); } finally { setConnectionSaving(false); } }; return ( Provider Settings Manage how each provider connects and, when supported, which backend the multimodel runtime should use.
Provider
setSelectedProviderId(value as CliProviderId)} >
{providers.map((provider) => ( {provider.displayName} ))}
{showSelectedProviderSummary && selectedProvider ? (
{selectedProvider.displayName} {selectedProvider.authenticated ? `Using ${formatProviderAuthMethodLabelForProvider( selectedProvider.providerId, selectedProvider.authMethod )}` : selectedProvider.statusMessage || 'Not connected'} {managedRuntimeSummary && !hideConnectionMethodMeta ? ( {managedRuntimeSummary} ) : runtimeSummary ? ( Runtime: {runtimeSummary} ) : null}
{selectedProvider.detailMessage ? (
{selectedProvider.detailMessage}
) : null} {selectedProvider.externalRuntimeDiagnostics && selectedProvider.externalRuntimeDiagnostics.length > 0 ? (
{selectedProvider.externalRuntimeDiagnostics.slice(0, 3).map((diagnostic) => (
{diagnostic.label}:{' '} {diagnostic.statusMessage ?? (diagnostic.detected ? 'detected' : 'missing')} {diagnostic.detailMessage ? ` - ${diagnostic.detailMessage}` : ''}
))}
) : null}
) : null} {selectedProvider ? (
Connection
{getConnectionDescription(selectedProvider)}
{connectionProgressMessage ? (
{connectionProgressMessage}
) : null}
{canRequestSubscriptionLogin ? ( ) : null}
{showConnectionMethodCards ? (
void handleAuthModeChange(authMode)} /> {connectionMethodCardsHint ? (
{connectionMethodCardsHint}
) : null}
) : configurableAuthModes.length > 0 && configuredAuthMode ? (
{getAuthModeDescription(selectedProvider.providerId, configuredAuthMode)}
) : null}
{configuredAuthMode && !hideConnectionMethodMeta ? ( Mode:{' '} {formatProviderAuthModeLabelForProvider( selectedProvider.providerId, configuredAuthMode )} ) : null} {connectionStatusLabel ? ( {connectionStatusLabel} ) : null} {selectedProvider.connection?.apiKeyConfigured && !showApiKeySection ? ( {selectedProvider.connection.apiKeySourceLabel} ) : null}
{selectedProvider.providerId === 'anthropic' ? (
Fast mode default
Apply Claude Code Fast mode by default for new Anthropic team launches when the resolved model and runtime allow it.
{anthropicFastModeSupported ? (
{[ { enabled: false, label: 'Default Off' }, { enabled: true, label: 'Prefer Fast' }, ].map((option) => ( ))}
) : null}
{anthropicFastModeSupported && anthropicFastModeAvailable ? anthropicFastModeEnabled ? 'New Anthropic launches will request Fast mode by default when the resolved model supports it.' : 'New Anthropic launches stay on normal speed unless a team explicitly enables Fast mode.' : anthropicFastModeDisabledReason}
) : null} {selectedProvider.providerId === 'codex' ? (
ChatGPT account
Manage the local Codex app-server account session that powers subscription-backed native launches.
{codexLoginPending ? ( ) : codexHasActiveChatgptSession ? ( ) : ( )}
{codexHasActiveChatgptSession ? 'Connected' : codexNeedsReconnect ? 'Reconnect required' : codexLoginPending ? 'Login in progress' : 'Not connected'} {codexConnection ? ( App-server: {codexConnection.appServerState} ) : null} {codexConnection?.managedAccount?.planType ? ( Plan: {codexConnection.managedAccount.planType} ) : null} {codexConnection?.managedAccount?.email ? ( {codexConnection.managedAccount.email} ) : null}
{codexAccountPanelHint ? (
{codexAccountPanelHint}
) : null} {codexFastCapabilityHint ? (
{codexFastCapabilityHint}
) : null} {codexConnection?.rateLimits ? (
These percentages show used quota, not remaining quota.{' '} {formatCodexUsageExplanation( codexConnection.rateLimits.primary?.usedPercent, codexConnection.rateLimits.primary?.windowDurationMins )} {codexConnection.rateLimits.secondary ? ` Weekly limits are shown separately in the ${ formatCodexWindowDurationLong( codexConnection.rateLimits.secondary.windowDurationMins ) ?? 'secondary' } window.` : ''}
{codexConnection.rateLimits.secondary ? ( ) : (
Weekly window
Weekly used (1w)
Not reported
Codex did not return a secondary window for this account snapshot.
)}
Credits
{formatCodexCreditsValue(codexConnection.rateLimits.credits)}
Credits are shown separately from window-based subscription usage and may be unavailable for plan-backed ChatGPT sessions.
) : null}
) : null} {showApiKeySection && apiKeyConfig ? (
{apiKeyConfig.title}
{apiKeyConfig.description}
{!showApiKeyForm ? ( ) : null}
{selectedProvider.connection?.apiKeyConfigured || selectedApiKey ? 'Configured' : 'Not configured'} {selectedApiKey ? ( {selectedApiKey.maskedValue} ยท {selectedApiKey.scope} ) : selectedProvider.connection?.apiKeySource === 'environment' ? ( {selectedProvider.connection.apiKeySourceLabel} ) : null} {apiKeyStorageStatus && selectedApiKey ? ( Stored in {apiKeyStorageStatus.backend} ) : null}
{showApiKeyForm ? (
setApiKeyValue(e.target.value)} placeholder={apiKeyConfig.placeholder} className="h-9 text-sm" autoFocus />
{(apiKeyError || apiKeysError) && (
{apiKeyError ?? apiKeysError}
)}
{selectedApiKey ? ( ) : ( )}
) : null}
) : null} {connectionError ? (
{connectionError}
) : null} {connectionAlert ? (
{connectionAlert}
) : null} {apiKeysLoading && !selectedApiKey ? (
Loading stored credentials...
) : null}
) : null} {selectedProvider && canConfigureRuntime ? (
Runtime
{getRuntimeDescription(selectedProvider)}
void handleRuntimeBackendSelect(providerId, backendId) } /> {runtimeSaving ? (
Updating runtime...
) : null} {runtimeError ? (
{runtimeError}
) : null}
) : null}
); };