feat: enhance Codex authentication and API key management

- Introduced a new method to check Codex authentication status, allowing for better handling of API keys and OAuth tokens.
- Updated API key management to include OpenAI, enabling users to manage their keys more effectively.
- Enhanced the CodexProvider to support session ID tracking and deduplication of text blocks in assistant messages.
- Improved error handling and logging in authentication routes, providing clearer feedback to users.

These changes improve the overall user experience and security of the Codex integration, ensuring smoother authentication processes and better management of API keys.
This commit is contained in:
DhanushSantosh
2026-01-07 22:49:30 +05:30
parent 2250367ddc
commit 24ea10e818
18 changed files with 837 additions and 61 deletions

View File

@@ -14,8 +14,15 @@ import { useNavigate } from '@tanstack/react-router';
export function ApiKeysSection() {
const { apiKeys, setApiKeys } = useAppStore();
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
const {
claudeAuthStatus,
setClaudeAuthStatus,
codexAuthStatus,
setCodexAuthStatus,
setSetupComplete,
} = useSetupStore();
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const [isDeletingOpenaiKey, setIsDeletingOpenaiKey] = useState(false);
const navigate = useNavigate();
const { providerConfigParams, handleSave, saved } = useApiKeyManagement();
@@ -51,6 +58,34 @@ export function ApiKeysSection() {
}
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
// Delete OpenAI API key
const deleteOpenaiKey = useCallback(async () => {
setIsDeletingOpenaiKey(true);
try {
const api = getElectronAPI();
if (!api.setup?.deleteApiKey) {
toast.error('Delete API not available');
return;
}
const result = await api.setup.deleteApiKey('openai');
if (result.success) {
setApiKeys({ ...apiKeys, openai: '' });
setCodexAuthStatus({
authenticated: false,
method: 'none',
});
toast.success('OpenAI API key deleted');
} else {
toast.error(result.error || 'Failed to delete API key');
}
} catch (error) {
toast.error('Failed to delete API key');
} finally {
setIsDeletingOpenaiKey(false);
}
}, [apiKeys, setApiKeys, setCodexAuthStatus]);
// Open setup wizard
const openSetupWizard = useCallback(() => {
setSetupComplete(false);
@@ -137,6 +172,23 @@ export function ApiKeysSection() {
Delete Anthropic Key
</Button>
)}
{apiKeys.openai && (
<Button
onClick={deleteOpenaiKey}
disabled={isDeletingOpenaiKey}
variant="outline"
className="h-10 border-red-500/30 text-red-500 hover:bg-red-500/10 hover:border-red-500/50"
data-testid="delete-openai-key"
>
{isDeletingOpenaiKey ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Trash2 className="w-4 h-4 mr-2" />
)}
Delete OpenAI Key
</Button>
)}
</div>
</div>
</div>

View File

@@ -15,6 +15,7 @@ interface TestResult {
interface ApiKeyStatus {
hasAnthropicKey: boolean;
hasGoogleKey: boolean;
hasOpenaiKey: boolean;
}
/**
@@ -27,16 +28,20 @@ export function useApiKeyManagement() {
// API key values
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
// Visibility toggles
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
const [showGoogleKey, setShowGoogleKey] = useState(false);
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
// Test connection states
const [testingConnection, setTestingConnection] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(null);
const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false);
const [openaiTestResult, setOpenaiTestResult] = useState<TestResult | null>(null);
// API key status from environment
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
@@ -48,6 +53,7 @@ export function useApiKeyManagement() {
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
setGoogleKey(apiKeys.google);
setOpenaiKey(apiKeys.openai);
}, [apiKeys]);
// Check API key status from environment on mount
@@ -61,6 +67,7 @@ export function useApiKeyManagement() {
setApiKeyStatus({
hasAnthropicKey: status.hasAnthropicKey,
hasGoogleKey: status.hasGoogleKey,
hasOpenaiKey: status.hasOpenaiKey,
});
}
} catch (error) {
@@ -136,11 +143,42 @@ export function useApiKeyManagement() {
setTestingGeminiConnection(false);
};
// Test OpenAI/Codex connection
const handleTestOpenaiConnection = async () => {
setTestingOpenaiConnection(true);
setOpenaiTestResult(null);
try {
const api = getElectronAPI();
const data = await api.setup.verifyCodexAuth('api_key', openaiKey);
if (data.success && data.authenticated) {
setOpenaiTestResult({
success: true,
message: 'Connection successful! Codex responded.',
});
} else {
setOpenaiTestResult({
success: false,
message: data.error || 'Failed to connect to OpenAI API.',
});
}
} catch {
setOpenaiTestResult({
success: false,
message: 'Network error. Please check your connection.',
});
} finally {
setTestingOpenaiConnection(false);
}
};
// Save API keys
const handleSave = () => {
setApiKeys({
anthropic: anthropicKey,
google: googleKey,
openai: openaiKey,
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
@@ -167,6 +205,15 @@ export function useApiKeyManagement() {
onTest: handleTestGeminiConnection,
result: geminiTestResult,
},
openai: {
value: openaiKey,
setValue: setOpenaiKey,
show: showOpenaiKey,
setShow: setShowOpenaiKey,
testing: testingOpenaiConnection,
onTest: handleTestOpenaiConnection,
result: openaiTestResult,
},
};
return {

View File

@@ -0,0 +1,183 @@
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Cpu } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CodexModelId } from '@automaker/types';
import { CODEX_MODEL_MAP } from '@automaker/types';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CodexModelConfigurationProps {
enabledCodexModels: CodexModelId[];
codexDefaultModel: CodexModelId;
isSaving: boolean;
onDefaultModelChange: (model: CodexModelId) => void;
onModelToggle: (model: CodexModelId, enabled: boolean) => void;
}
interface CodexModelInfo {
id: CodexModelId;
label: string;
description: string;
}
const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
'gpt-5.2-codex': {
id: 'gpt-5.2-codex',
label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering',
},
'gpt-5-codex': {
id: 'gpt-5-codex',
label: 'GPT-5-Codex',
description: 'Purpose-built for Codex CLI with versatile tool use',
},
'gpt-5-codex-mini': {
id: 'gpt-5-codex-mini',
label: 'GPT-5-Codex-Mini',
description: 'Faster workflows optimized for low-latency code Q&A and editing',
},
'codex-1': {
id: 'codex-1',
label: 'Codex-1',
description: 'Version of o3 optimized for software engineering',
},
'codex-mini-latest': {
id: 'codex-mini-latest',
label: 'Codex-Mini-Latest',
description: 'Version of o4-mini for Codex, optimized for faster workflows',
},
'gpt-5': {
id: 'gpt-5',
label: 'GPT-5',
description: 'GPT-5 base flagship model',
},
};
export function CodexModelConfiguration({
enabledCodexModels,
codexDefaultModel,
isSaving,
onDefaultModelChange,
onModelToggle,
}: CodexModelConfigurationProps) {
const availableModels = Object.values(CODEX_MODEL_INFO);
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<OpenAIIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Model Configuration
</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure which Codex models are available in the feature modal
</p>
</div>
<div className="p-6 space-y-6">
<div className="space-y-2">
<Label>Default Model</Label>
<Select
value={codexDefaultModel}
onValueChange={(v) => onDefaultModelChange(v as CodexModelId)}
disabled={isSaving}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableModels.map((model) => (
<SelectItem key={model.id} value={model.id}>
<div className="flex items-center gap-2">
<span>{model.label}</span>
{supportsReasoningEffort(model.id) && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<Label>Available Models</Label>
<div className="grid gap-3">
{availableModels.map((model) => {
const isEnabled = enabledCodexModels.includes(model.id);
const isDefault = model.id === codexDefaultModel;
return (
<div
key={model.id}
className="flex items-center justify-between p-3 rounded-xl border border-border/50 bg-card/50 hover:bg-accent/30 transition-colors"
>
<div className="flex items-center gap-3">
<Checkbox
checked={isEnabled}
onCheckedChange={(checked) => onModelToggle(model.id, !!checked)}
disabled={isSaving || isDefault}
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{model.label}</span>
{supportsReasoningEffort(model.id) && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
{isDefault && (
<Badge variant="secondary" className="text-xs">
Default
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">{model.description}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
function getModelDisplayName(modelId: string): string {
const displayNames: Record<string, string> = {
'gpt-5.2-codex': 'GPT-5.2-Codex',
'gpt-5-codex': 'GPT-5-Codex',
'gpt-5-codex-mini': 'GPT-5-Codex-Mini',
'codex-1': 'Codex-1',
'codex-mini-latest': 'Codex-Mini-Latest',
'gpt-5': 'GPT-5',
};
return displayNames[modelId] || modelId;
}
function supportsReasoningEffort(modelId: string): boolean {
const reasoningModels = ['gpt-5.2-codex', 'gpt-5-codex', 'gpt-5', 'codex-1'];
return reasoningModels.includes(modelId);
}

View File

@@ -1,27 +1,35 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { CodexCliStatus } from '../cli-status/codex-cli-status';
import { CodexSettings } from '../codex/codex-settings';
import { CodexUsageSection } from '../codex/codex-usage-section';
import { Info } from 'lucide-react';
import { CodexModelConfiguration } from './codex-model-configuration';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger';
import type { CliStatus as SharedCliStatus } from '../shared/types';
import type { CodexModelId } from '@automaker/types';
const logger = createLogger('CodexSettings');
export function CodexSettingsTab() {
// TODO: Add these to app-store
const [codexAutoLoadAgents, setCodexAutoLoadAgents] = useState(false);
const [codexSandboxMode, setCodexSandboxMode] = useState<
'read-only' | 'workspace-write' | 'danger-full-access'
>('read-only');
const [codexApprovalPolicy, setCodexApprovalPolicy] = useState<
'untrusted' | 'on-failure' | 'on-request' | 'never'
>('untrusted');
const [codexEnableWebSearch, setCodexEnableWebSearch] = useState(false);
const [codexEnableImages, setCodexEnableImages] = useState(false);
const {
codexAutoLoadAgents,
codexSandboxMode,
codexApprovalPolicy,
codexEnableWebSearch,
codexEnableImages,
enabledCodexModels,
codexDefaultModel,
setCodexAutoLoadAgents,
setCodexSandboxMode,
setCodexApprovalPolicy,
setCodexEnableWebSearch,
setCodexEnableImages,
setEnabledCodexModels,
setCodexDefaultModel,
toggleCodexModel,
} = useAppStore();
const {
codexAuthStatus,
@@ -32,8 +40,8 @@ export function CodexSettingsTab() {
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
const [displayCliStatus, setDisplayCliStatus] = useState<SharedCliStatus | null>(null);
const [isSaving, setIsSaving] = useState(false);
// Convert setup-store CliStatus to shared/types CliStatus for display
const codexCliStatus: SharedCliStatus | null =
displayCliStatus ||
(setupCliStatus
@@ -46,28 +54,28 @@ export function CodexSettingsTab() {
}
: null);
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
// Load Codex CLI status on mount
useEffect(() => {
const checkCodexStatus = async () => {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
const result = await api.setup.getCodexStatus();
if (result.success) {
// Update setup store
try {
const result = await api.setup.getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
method: result.auth?.method || 'none',
});
// Update display status
setDisplayCliStatus({
success: true,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version || undefined,
path: result.path || undefined,
});
if (result.auth) {
setCodexAuthStatus({
authenticated: result.auth.authenticated,
@@ -80,6 +88,42 @@ export function CodexSettingsTab() {
hasApiKey: result.auth.hasApiKey,
});
}
} catch (error) {
logger.error('Failed to check Codex CLI status:', error);
}
}
};
checkCodexStatus();
}, [setCodexCliStatus, setCodexAuthStatus]);
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
const result = await api.setup.getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
method: result.auth?.method || 'none',
});
if (result.auth) {
setCodexAuthStatus({
authenticated: result.auth.authenticated,
method: result.auth.method as 'cli_authenticated' | 'api_key' | 'api_key_env' | 'none',
hasAuthFile: result.auth.method === 'cli_authenticated',
hasApiKey: result.auth.hasApiKey,
});
}
}
} catch (error) {
@@ -89,27 +133,50 @@ export function CodexSettingsTab() {
}
}, [setCodexCliStatus, setCodexAuthStatus]);
// Show usage tracking when CLI is authenticated
const handleDefaultModelChange = useCallback(
(model: CodexModelId) => {
setIsSaving(true);
try {
setCodexDefaultModel(model);
} finally {
setIsSaving(false);
}
},
[setCodexDefaultModel]
);
const handleModelToggle = useCallback(
(model: CodexModelId, enabled: boolean) => {
setIsSaving(true);
try {
toggleCodexModel(model, enabled);
} finally {
setIsSaving(false);
}
},
[toggleCodexModel]
);
const showUsageTracking = codexAuthStatus?.authenticated ?? false;
return (
<div className="space-y-6">
{/* Usage Info */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<Info className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
<div className="text-sm text-emerald-400/90">
<span className="font-medium">OpenAI via Codex CLI</span>
<p className="text-xs text-emerald-400/70 mt-1">
Access GPT models with tool support for advanced coding workflows.
</p>
</div>
</div>
<CodexCliStatus
status={codexCliStatus}
isChecking={isCheckingCodexCli}
onRefresh={handleRefreshCodexCli}
/>
{showUsageTracking && <CodexUsageSection />}
<CodexModelConfiguration
enabledCodexModels={enabledCodexModels}
codexDefaultModel={codexDefaultModel}
isSaving={isSaving}
onDefaultModelChange={handleDefaultModelChange}
onModelToggle={handleModelToggle}
/>
<CodexSettings
autoLoadCodexAgents={codexAutoLoadAgents}
codexSandboxMode={codexSandboxMode}
@@ -122,7 +189,6 @@ export function CodexSettingsTab() {
onCodexEnableWebSearchChange={setCodexEnableWebSearch}
onCodexEnableImagesChange={setCodexEnableImages}
/>
{showUsageTracking && <CodexUsageSection />}
</div>
);
}

View File

@@ -75,7 +75,10 @@ interface CliSetupConfig {
buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
statusApi: () => Promise<any>;
installApi: () => Promise<any>;
verifyAuthApi: (method: 'cli' | 'api_key') => Promise<{
verifyAuthApi: (
method: 'cli' | 'api_key',
apiKey?: string
) => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
@@ -194,7 +197,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup
setApiKeyVerificationError(null);
try {
const result = await config.verifyAuthApi('api_key');
const result = await config.verifyAuthApi('api_key', apiKey);
const hasLimitOrBillingError =
result.error?.toLowerCase().includes('limit reached') ||

View File

@@ -31,8 +31,8 @@ export function CodexSetupStep({ onNext, onBack, onSkip }: CodexSetupStepProps)
);
const verifyAuthApi = useCallback(
(method: 'cli' | 'api_key') =>
getElectronAPI().setup?.verifyCodexAuth(method) || Promise.reject(),
(method: 'cli' | 'api_key', apiKey?: string) =>
getElectronAPI().setup?.verifyCodexAuth(method, apiKey) || Promise.reject(),
[]
);