diff --git a/apps/server/src/providers/codex-provider.ts b/apps/server/src/providers/codex-provider.ts index 2e3962a0..e0f38ee9 100644 --- a/apps/server/src/providers/codex-provider.ts +++ b/apps/server/src/providers/codex-provider.ts @@ -45,6 +45,7 @@ import { getCodexTodoToolName, } from './codex-tool-mapping.js'; import { SettingsService } from '../services/settings-service.js'; +import { createTempEnvOverride } from '../lib/auth-utils.js'; import { checkSandboxCompatibility } from '../lib/sdk-options.js'; import { CODEX_MODELS } from './codex-models.js'; @@ -142,6 +143,7 @@ type CodexExecutionMode = typeof CODEX_EXECUTION_MODE_CLI | typeof CODEX_EXECUTI type CodexExecutionPlan = { mode: CodexExecutionMode; cliPath: string | null; + openAiApiKey?: string | null; }; const ALLOWED_ENV_VARS = [ @@ -166,6 +168,22 @@ function buildEnv(): Record { return env; } +async function resolveOpenAiApiKey(): Promise { + const envKey = process.env[OPENAI_API_KEY_ENV]; + if (envKey) { + return envKey; + } + + try { + const settingsService = new SettingsService(getCodexSettingsDir()); + const credentials = await settingsService.getCredentials(); + const storedKey = credentials.apiKeys.openai?.trim(); + return storedKey ? storedKey : null; + } catch { + return null; + } +} + function hasMcpServersConfigured(options: ExecuteOptions): boolean { return Boolean(options.mcpServers && Object.keys(options.mcpServers).length > 0); } @@ -181,18 +199,21 @@ function isSdkEligible(options: ExecuteOptions): boolean { async function resolveCodexExecutionPlan(options: ExecuteOptions): Promise { const cliPath = await findCodexCliPath(); const authIndicators = await getCodexAuthIndicators(); - const hasApiKey = Boolean(process.env[OPENAI_API_KEY_ENV]); + const openAiApiKey = await resolveOpenAiApiKey(); + const hasApiKey = Boolean(openAiApiKey); const cliAuthenticated = authIndicators.hasOAuthToken || authIndicators.hasApiKey || hasApiKey; const sdkEligible = isSdkEligible(options); const cliAvailable = Boolean(cliPath); + if (hasApiKey) { + return { + mode: CODEX_EXECUTION_MODE_SDK, + cliPath, + openAiApiKey, + }; + } + if (sdkEligible) { - if (hasApiKey) { - return { - mode: CODEX_EXECUTION_MODE_SDK, - cliPath, - }; - } if (!cliAvailable) { throw new Error(ERROR_CODEX_SDK_AUTH_REQUIRED); } @@ -209,6 +230,7 @@ async function resolveCodexExecutionPlan(options: ExecuteOptions): Promise { const cliPath = await findCodexCliPath(); - const hasApiKey = !!process.env[OPENAI_API_KEY_ENV]; + const hasApiKey = Boolean(await resolveOpenAiApiKey()); const authIndicators = await getCodexAuthIndicators(); const installed = !!cliPath; @@ -1013,7 +1047,7 @@ export class CodexProvider extends BaseProvider { */ async checkAuth(): Promise { const cliPath = await findCodexCliPath(); - const hasApiKey = !!process.env[OPENAI_API_KEY_ENV]; + const hasApiKey = Boolean(await resolveOpenAiApiKey()); const authIndicators = await getCodexAuthIndicators(); // Check for API key in environment diff --git a/apps/server/tests/unit/providers/codex-provider.test.ts b/apps/server/tests/unit/providers/codex-provider.test.ts index ada1aae1..6ca69d86 100644 --- a/apps/server/tests/unit/providers/codex-provider.test.ts +++ b/apps/server/tests/unit/providers/codex-provider.test.ts @@ -257,7 +257,7 @@ describe('codex-provider.ts', () => { expect(results[1].result).toBe('Hello from SDK'); }); - it('uses the CLI when tools are requested even if an API key is present', async () => { + it('uses the SDK when API key is present, even for tool requests (to avoid OAuth issues)', async () => { process.env[OPENAI_API_KEY_ENV] = 'sk-test'; vi.mocked(spawnJSONLProcess).mockReturnValue((async function* () {})()); @@ -270,8 +270,8 @@ describe('codex-provider.ts', () => { }) ); - expect(codexRunMock).not.toHaveBeenCalled(); - expect(spawnJSONLProcess).toHaveBeenCalled(); + expect(codexRunMock).toHaveBeenCalled(); + expect(spawnJSONLProcess).not.toHaveBeenCalled(); }); it('falls back to CLI when no tools are requested and no API key is available', async () => {